diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000000..4b1147e79f --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,14 @@ +name: Trigger Docker build on release +on: + release: + types: [released] +jobs: + curl: + runs-on: ubuntu-latest + container: + image: alpine:latest + steps: + - 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 diff --git a/CODEOWNERS b/CODEOWNERS index 5e1113d34f..7cf65a7a73 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,17 +3,16 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, -* @nabinhait -manufacturing/ @rohitwaghchaure +manufacturing/ @rohitwaghchaure @marination accounts/ @deepeshgarg007 @nextchamp-saqib -loan_management/ @deepeshgarg007 -pos* @nextchamp-saqib -assets/ @nextchamp-saqib +loan_management/ @deepeshgarg007 @rohitwaghchaure +pos* @nextchamp-saqib @rohitwaghchaure +assets/ @nextchamp-saqib @deepeshgarg007 stock/ @marination @rohitwaghchaure -buying/ @marination @rohitwaghchaure -hr/ @Anurag810 -projects/ @hrwX -support/ @hrwX -healthcare/ @ruchamahabal -erpnext_integrations/ @Mangesh-Khairnar +buying/ @marination @deepeshgarg007 +hr/ @Anurag810 @rohitwaghchaure +projects/ @hrwX @nextchamp-saqib +support/ @hrwX @marination +healthcare/ @ruchamahabal @marination +erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib requirements.txt @gavindsouza diff --git a/erpnext/accounts/report/ordered_items_to_be_billed/__init__.py b/erpnext/accounts/accounts similarity index 100% rename from erpnext/accounts/report/ordered_items_to_be_billed/__init__.py rename to erpnext/accounts/accounts diff --git a/erpnext/accounts/dashboard_fixtures.py b/erpnext/accounts/dashboard_fixtures.py index cdd166134f..b2abffc79d 100644 --- a/erpnext/accounts/dashboard_fixtures.py +++ b/erpnext/accounts/dashboard_fixtures.py @@ -1,127 +1,284 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from erpnext import get_default_company import frappe import json +from frappe.utils import nowdate, add_months, get_date_str +from frappe import _ +from erpnext.accounts.utils import get_fiscal_year, get_account_name, FiscalYearError +def _get_fiscal_year(date=None): + try: + fiscal_year = get_fiscal_year(date=nowdate(), as_dict=True) + return fiscal_year + + except FiscalYearError: + #if no fiscal year for current date then get default fiscal year + try: + fiscal_year = get_fiscal_year(as_dict=True) + return fiscal_year + + except FiscalYearError: + #if still no fiscal year found then no accounting data created, return + return None + +def get_company_for_dashboards(): + company = frappe.defaults.get_defaults().company + if company: + return company + else: + company_list = frappe.get_list("Company") + if company_list: + return company_list[0].name + return None def get_data(): - data = frappe._dict({ - "dashboards": [], - "charts": [] + + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(fiscal_year), + "number_cards": get_number_cards(fiscal_year) }) - company = get_company_for_dashboards() - if company: - company_doc = frappe.get_doc("Company", company) - data.dashboards = get_dashboards() - data.charts = get_charts(company_doc) - return data def get_dashboards(): return [{ "name": "Accounts", "dashboard_name": "Accounts", + "doctype": "Dashboard", "charts": [ - { "chart": "Outgoing Bills (Sales Invoice)" }, - { "chart": "Incoming Bills (Purchase Invoice)" }, - { "chart": "Bank Balance" }, - { "chart": "Income" }, - { "chart": "Expenses" } + { "chart": "Profit and Loss" , "width": "Full"}, + { "chart": "Incoming Bills (Purchase Invoice)", "width": "Half"}, + { "chart": "Outgoing Bills (Sales Invoice)", "width": "Half"}, + { "chart": "Accounts Receivable Ageing", "width": "Half"}, + { "chart": "Accounts Payable Ageing", "width": "Half"}, + { "chart": "Budget Variance", "width": "Full"}, + { "chart": "Bank Balance", "width": "Full"} + ], + "cards": [ + {"card": "Total Outgoing Bills"}, + {"card": "Total Incoming Bills"}, + {"card": "Total Incoming Payment"}, + {"card": "Total Outgoing Payment"} ] }] -def get_charts(company): - income_account = company.default_income_account or get_account("Income Account", company.name) - expense_account = company.default_expense_account or get_account("Expense Account", company.name) - bank_account = company.default_bank_account or get_account("Bank", company.name) +def get_charts(fiscal_year): + company = frappe.get_doc("Company", get_company_for_dashboards()) + bank_account = company.default_bank_account or get_account_name("Bank", company=company.name) + default_cost_center = company.cost_center return [ { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Income", - "chart_name": "Income", - "timespan": "Last Year", - "color": None, - "filters_json": json.dumps({"company": company.name, "account": income_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, + "doctype": "Dashboard Charts", + "name": "Profit and Loss", "owner": "Administrator", - "type": "Line" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Expenses", - "chart_name": "Expenses", - "timespan": "Last Year", - "color": None, - "filters_json": json.dumps({"company": company.name, "account": expense_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Bank Balance", - "chart_name": "Bank Balance", - "timespan": "Last Year", - "color": "#ffb868", - "filters_json": json.dumps({"company": company.name, "account": bank_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line" + "report_name": "Profit and Loss Statement", + "filters_json": json.dumps({ + "company": company.name, + "filter_based_on": "Fiscal Year", + "from_fiscal_year": fiscal_year.get('name'), + "to_fiscal_year": fiscal_year.get('name'), + "periodicity": "Monthly", + "include_default_book_entries": 1 + }), + "type": "Bar", + 'timeseries': 0, + "chart_type": "Report", + "chart_name": _("Profit and Loss"), + "is_custom": 1, + "is_public": 1 }, { "doctype": "Dashboard Chart", "time_interval": "Monthly", "name": "Incoming Bills (Purchase Invoice)", - "chart_name": "Incoming Bills (Purchase Invoice)", + "chart_name": _("Incoming Bills (Purchase Invoice)"), "timespan": "Last Year", "color": "#a83333", - "value_based_on": "base_grand_total", - "filters_json": json.dumps({}), + "value_based_on": "base_net_total", + "filters_json": json.dumps([["Purchase Invoice", "docstatus", "=", 1]]), "chart_type": "Sum", "timeseries": 1, "based_on": "posting_date", "owner": "Administrator", "document_type": "Purchase Invoice", - "type": "Bar" + "type": "Bar", + "width": "Half", + "is_public": 1 }, { "doctype": "Dashboard Chart", - "time_interval": "Monthly", "name": "Outgoing Bills (Sales Invoice)", - "chart_name": "Outgoing Bills (Sales Invoice)", + "time_interval": "Monthly", + "chart_name": _("Outgoing Bills (Sales Invoice)"), "timespan": "Last Year", "color": "#7b933d", - "value_based_on": "base_grand_total", - "filters_json": json.dumps({}), + "value_based_on": "base_net_total", + "filters_json": json.dumps([["Sales Invoice", "docstatus", "=", 1]]), "chart_type": "Sum", "timeseries": 1, "based_on": "posting_date", "owner": "Administrator", "document_type": "Sales Invoice", - "type": "Bar" - } + "type": "Bar", + "width": "Half", + "is_public": 1 + }, + { + "doctype": "Dashboard Charts", + "name": "Accounts Receivable Ageing", + "owner": "Administrator", + "report_name": "Accounts Receivable", + "filters_json": json.dumps({ + "company": company.name, + "report_date": nowdate(), + "ageing_based_on": "Due Date", + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120 + }), + "type": "Donut", + 'timeseries': 0, + "chart_type": "Report", + "chart_name": _("Accounts Receivable Ageing"), + "is_custom": 1, + "is_public": 1 + }, + { + "doctype": "Dashboard Charts", + "name": "Accounts Payable Ageing", + "owner": "Administrator", + "report_name": "Accounts Payable", + "filters_json": json.dumps({ + "company": company.name, + "report_date": nowdate(), + "ageing_based_on": "Due Date", + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120 + }), + "type": "Donut", + 'timeseries': 0, + "chart_type": "Report", + "chart_name": _("Accounts Payable Ageing"), + "is_custom": 1, + "is_public": 1 + }, + { + "doctype": "Dashboard Charts", + "name": "Budget Variance", + "owner": "Administrator", + "report_name": "Budget Variance Report", + "filters_json": json.dumps({ + "company": company.name, + "from_fiscal_year": fiscal_year.get('name'), + "to_fiscal_year": fiscal_year.get('name'), + "period": "Monthly", + "budget_against": "Cost Center" + }), + "type": "Bar", + "timeseries": 0, + "chart_type": "Report", + "chart_name": _("Budget Variance"), + "is_custom": 1, + "is_public": 1 + }, + { + "doctype": "Dashboard Charts", + "name": "Bank Balance", + "time_interval": "Quarterly", + "chart_name": "Bank Balance", + "timespan": "Last Year", + "filters_json": json.dumps({ + "company": company.name, + "account": bank_account + }), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line", + "width": "Half", + "is_public": 1 + }, ] -def get_account(account_type, company): - accounts = frappe.get_list("Account", filters={"account_type": account_type, "company": company}) - if accounts: - return accounts[0].name +def get_number_cards(fiscal_year): -def get_company_for_dashboards(): - company = get_default_company() - if not company: - company_list = frappe.get_list("Company") - if company_list: - company = company_list[0].name - return company + 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 [ + { + "doctype": "Number Card", + "document_type": "Payment Entry", + "name": "Total Incoming Payment", + "filters_json": json.dumps([ + ['Payment Entry', 'docstatus', '=', 1], + ['Payment Entry', 'posting_date', 'between', [year_start_date, year_end_date]], + ['Payment Entry', 'payment_type', '=', 'Receive'] + ]), + "label": _("Total Incoming Payment"), + "function": "Sum", + "aggregate_function_based_on": "base_received_amount", + "is_public": 1, + "is_custom": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Payment Entry", + "name": "Total Outgoing Payment", + "filters_json": json.dumps([ + ['Payment Entry', 'docstatus', '=', 1], + ['Payment Entry', 'posting_date', 'between', [year_start_date, year_end_date]], + ['Payment Entry', 'payment_type', '=', 'Pay'] + ]), + "label": _("Total Outgoing Payment"), + "function": "Sum", + "aggregate_function_based_on": "base_paid_amount", + "is_public": 1, + "is_custom": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Sales Invoice", + "name": "Total Outgoing Bills", + "filters_json": json.dumps([ + ['Sales Invoice', 'docstatus', '=', 1], + ['Sales Invoice', 'posting_date', 'between', [year_start_date, year_end_date]] + ]), + "label": _("Total Outgoing Bills"), + "function": "Sum", + "aggregate_function_based_on": "base_net_total", + "is_public": 1, + "is_custom": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "doctype": "Number Card", + "document_type": "Purchase Invoice", + "name": "Total Incoming Bills", + "filters_json": json.dumps([ + ['Purchase Invoice', 'docstatus', '=', 1], + ['Purchase Invoice', 'posting_date', 'between', [year_start_date, year_end_date]] + ]), + "label": _("Total Incoming Bills"), + "function": "Sum", + "aggregate_function_based_on": "base_net_total", + "is_public": 1, + "is_custom": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + } + ] diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index b57e6783ce..d5ab1c1704 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -2,10 +2,11 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import date_diff, add_months, today, getdate, add_days, flt, get_last_day, cint, get_link_to_form +from frappe.utils import date_diff, add_months, today, getdate, add_days, flt, get_last_day, get_first_day, cint, get_link_to_form, rounded from erpnext.accounts.utils import get_account_currency from frappe.email import sendmail_to_system_managers from frappe.utils.background_jobs import enqueue +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions def validate_service_stop_date(doc): ''' Validates service_stop_date for Purchase Invoice and Sales Invoice ''' @@ -109,6 +110,18 @@ def get_booking_dates(doc, item, posting_date=None): order by posting_date desc limit 1 ''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) + 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) + + 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): + prev_gl_entry = prev_gl_via_je + if prev_gl_entry: start_date = getdate(add_days(prev_gl_entry[0].posting_date, 1)) else: @@ -130,14 +143,48 @@ def get_booking_dates(doc, item, posting_date=None): else: return None, None, None -def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency): - if doc.doctype == "Sales Invoice": - total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency" - deferred_account = "deferred_revenue_account" - else: - total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency" - deferred_account = "deferred_expense_account" +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 + + 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) + 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: + amount = base_amount + else: + 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))) + + 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: + amount = base_amount + else: + 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")) @@ -146,27 +193,55 @@ def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, a else: amount = flt(item.net_amount*total_booking_days/flt(total_days), item.precision("net_amount")) else: - 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 - 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) - already_booked_amount = gl_entries_details[0].total_credit if gl_entries_details else 0 + 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: - already_booked_amount_in_account_currency = gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0 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" + deferred_account = "deferred_revenue_account" + else: + total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency" + deferred_account = "deferred_expense_account" + + 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 + 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) + + 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) + + 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 + + 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 + + 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" - def _book_deferred_revenue_or_expense(item): + 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 @@ -181,28 +256,42 @@ 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 - amount, base_amount = calculate_amount(doc, item, last_gl_entry, - 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) - make_gl_entries(doc, credit_account, debit_account, against, - amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process) + 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) + else: + 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) + _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') for item in doc.get('items'): if item.get(enable_check): - _book_deferred_revenue_or_expense(item) + _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on) -def process_deferred_accounting(posting_date=today()): +def process_deferred_accounting(posting_date=None): ''' 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')): return @@ -278,3 +367,83 @@ def send_mail(deferred_process): and submit manually after resolving errors """).format(get_link_to_form('Process Deferred Accounting', deferred_process)) 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 + + 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' + + debit_entry = { + 'account': credit_account, + 'credit': base_amount, + 'credit_in_account_currency': amount, + 'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier', + 'party': against, + 'account_currency': account_currency, + 'reference_name': doc.name, + 'reference_type': doc.doctype, + 'reference_detail_no': item.name, + 'cost_center': cost_center, + 'project': project, + } + + credit_entry = { + 'account': debit_account, + 'debit': base_amount, + 'debit_in_account_currency': amount, + 'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier', + 'party': against, + 'account_currency': account_currency, + 'reference_name': doc.name, + 'reference_type': doc.doctype, + 'reference_detail_no': item.name, + 'cost_center': cost_center, + 'project': project, + } + + for dimension in get_accounting_dimensions(): + debit_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) + + try: + journal_entry.save() + + if submit: + journal_entry.submit() + except: + frappe.db.rollback() + traceback = frappe.get_traceback() + frappe.log_error(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']) + else: + 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': + return debit_account + else: + return credit_account + + diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 0d6aca65b1..31315e4c71 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -13,12 +13,12 @@ { "hidden": 0, "label": "Accounts Receivable", - "links": "[\n {\n \"description\": \"Bills raised to Customers.\",\n \"label\": \"Sales Invoice\",\n \"name\": \"Sales Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customer database.\",\n \"label\": \"Customer\",\n \"name\": \"Customer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Bank/Cash transactions against party or for internal transfer\",\n \"label\": \"Payment Entry\",\n \"name\": \"Payment Entry\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Payment Request\",\n \"label\": \"Payment Request\",\n \"name\": \"Payment Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Receivable\",\n \"name\": \"Accounts Receivable\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Receivable Summary\",\n \"name\": \"Accounts Receivable Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Register\",\n \"name\": \"Sales Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales Register\",\n \"name\": \"Item-wise Sales Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Ordered Items To Be Billed\",\n \"name\": \"Ordered Items To Be Billed\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Delivered Items To Be Billed\",\n \"name\": \"Delivered Items To Be Billed\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"description\": \"Bills raised to Customers.\",\n \"label\": \"Sales Invoice\",\n \"name\": \"Sales Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customer database.\",\n \"label\": \"Customer\",\n \"name\": \"Customer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Bank/Cash transactions against party or for internal transfer\",\n \"label\": \"Payment Entry\",\n \"name\": \"Payment Entry\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Payment Request\",\n \"label\": \"Payment Request\",\n \"name\": \"Payment Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Receivable\",\n \"name\": \"Accounts Receivable\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Receivable Summary\",\n \"name\": \"Accounts Receivable Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Register\",\n \"name\": \"Sales Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales Register\",\n \"name\": \"Item-wise Sales Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Delivered Items To Be Billed\",\n \"name\": \"Delivered Items To Be Billed\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, "label": "Accounts Payable", - "links": "[\n {\n \"description\": \"Bills raised by Suppliers.\",\n \"label\": \"Purchase Invoice\",\n \"name\": \"Purchase Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Supplier database.\",\n \"label\": \"Supplier\",\n \"name\": \"Supplier\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Bank/Cash transactions against party or for internal transfer\",\n \"label\": \"Payment Entry\",\n \"name\": \"Payment Entry\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Payable\",\n \"name\": \"Accounts Payable\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Payable Summary\",\n \"name\": \"Accounts Payable Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Purchase Register\",\n \"name\": \"Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Purchase Register\",\n \"name\": \"Item-wise Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Items To Be Billed\",\n \"name\": \"Purchase Order Items To Be Billed\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Received Items To Be Billed\",\n \"name\": \"Received Items To Be Billed\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"description\": \"Bills raised by Suppliers.\",\n \"label\": \"Purchase Invoice\",\n \"name\": \"Purchase Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Supplier database.\",\n \"label\": \"Supplier\",\n \"name\": \"Supplier\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Bank/Cash transactions against party or for internal transfer\",\n \"label\": \"Payment Entry\",\n \"name\": \"Payment Entry\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Payable\",\n \"name\": \"Accounts Payable\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Payable Summary\",\n \"name\": \"Accounts Payable Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Purchase Register\",\n \"name\": \"Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Purchase Register\",\n \"name\": \"Item-wise Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Received Items To Be Billed\",\n \"name\": \"Received Items To Be Billed\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -45,11 +45,6 @@ "label": "Bank Statement", "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" }, - { - "hidden": 0, - "links": "[\n {\n \"description\": \"Match non-linked Invoices and Payments.\",\n \"label\": \"Match Payments with Invoices\",\n \"name\": \"Payment Reconciliation\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Update bank payment dates with journals.\",\n \"label\": \"Update Bank Clearance Dates\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Invoice Discounting\",\n \"name\": \"Invoice Discounting\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Clearance Summary\",\n \"name\": \"Bank Clearance Summary\",\n \"type\": \"report\"\n },\n {\n \"label\": \"Bank Guarantee\",\n \"name\": \"Bank Guarantee\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup cheque dimensions for printing\",\n \"label\": \"Cheque Print Template\",\n \"name\": \"Cheque Print Template\",\n \"type\": \"doctype\"\n }\n]", - "title": "Banking and Payments" - }, { "hidden": 0, "label": "Subscription Management", @@ -89,8 +84,8 @@ "category": "Modules", "charts": [ { - "chart_name": "Bank Balance", - "label": "Bank Balance" + "chart_name": "Profit and Loss", + "label": "Profit and Loss" } ], "creation": "2020-03-02 15:41:59.515192", @@ -99,23 +94,34 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-04-29 12:17:34.844397", + "modified": "2020-06-19 12:42:44.054598", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", + "onboarding": "Accounts", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "shortcuts": [ { - "label": "Account", + "label": "Chart Of Accounts", "link_to": "Account", "type": "DocType" }, + { + "label": "Sales Invoice", + "link_to": "Sales Invoice", + "type": "DocType" + }, + { + "label": "Purchase Invoice", + "link_to": "Purchase Invoice", + "type": "DocType" + }, { "label": "Journal Entry", "link_to": "Journal Entry", @@ -136,15 +142,15 @@ "link_to": "General Ledger", "type": "Report" }, - { - "label": "Profit and Loss Statement", - "link_to": "Profit and Loss Statement", - "type": "Report" - }, { "label": "Trial Balance", "link_to": "Trial Balance", "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "Accounts", + "type": "Dashboard" } ] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json index af252e6191..d2659d429b 100644 --- a/erpnext/accounts/doctype/account/account.json +++ b/erpnext/accounts/doctype/account/account.json @@ -34,11 +34,15 @@ { "fieldname": "properties", "fieldtype": "Section Break", - "oldfieldtype": "Section Break" + "oldfieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break0", "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -49,7 +53,9 @@ "no_copy": 1, "oldfieldname": "account_name", "oldfieldtype": "Data", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "account_number", @@ -57,13 +63,17 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Account Number", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "is_group", "fieldtype": "Check", - "label": "Is Group" + "label": "Is Group", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -75,7 +85,9 @@ "options": "Company", "read_only": 1, "remember_last_selected_value": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "root_type", @@ -83,7 +95,9 @@ "in_standard_filter": 1, "label": "Root Type", "options": "\nAsset\nLiability\nIncome\nExpense\nEquity", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "report_type", @@ -91,24 +105,32 @@ "in_standard_filter": 1, "label": "Report Type", "options": "\nBalance Sheet\nProfit and Loss", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_group==0", "fieldname": "account_currency", "fieldtype": "Link", "label": "Currency", - "options": "Currency" + "options": "Currency", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "inter_company_account", "fieldtype": "Check", - "label": "Inter Company Account" + "label": "Inter Company Account", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -120,7 +142,9 @@ "oldfieldtype": "Link", "options": "Account", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "description": "Setting Account Type helps in selecting this Account in transactions.", @@ -130,7 +154,9 @@ "label": "Account Type", "oldfieldname": "account_type", "oldfieldtype": "Select", - "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nTax\nTemporary" + "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary", + "show_days": 1, + "show_seconds": 1 }, { "description": "Rate at which this tax is applied", @@ -138,7 +164,9 @@ "fieldtype": "Float", "label": "Rate", "oldfieldname": "tax_rate", - "oldfieldtype": "Currency" + "oldfieldtype": "Currency", + "show_days": 1, + "show_seconds": 1 }, { "description": "If the account is frozen, entries are allowed to restricted users.", @@ -147,13 +175,17 @@ "label": "Frozen", "oldfieldname": "freeze_account", "oldfieldtype": "Select", - "options": "No\nYes" + "options": "No\nYes", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "balance_must_be", "fieldtype": "Select", "label": "Balance must be", - "options": "\nDebit\nCredit" + "options": "\nDebit\nCredit", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "lft", @@ -162,7 +194,9 @@ "label": "Lft", "print_hide": 1, "read_only": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rgt", @@ -171,7 +205,9 @@ "label": "Rgt", "print_hide": 1, "read_only": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "old_parent", @@ -179,27 +215,33 @@ "hidden": 1, "label": "Old Parent", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "depends_on": "eval:(doc.report_type == 'Profit and Loss' && !doc.is_group)", "fieldname": "include_in_gross", "fieldtype": "Check", - "label": "Include in gross" + "label": "Include in gross", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "label": "Disable" + "label": "Disable", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-money", "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-03-18 17:57:52.063233", + "modified": "2020-06-11 15:15:54.338622", "modified_by": "Administrator", "module": "Accounts", "name": "Account", diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index f62d07668d..28b090bdad 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -14,6 +14,9 @@ frappe.treeview_settings["Account"] = { on_change: function() { var me = frappe.treeview_settings['Account'].treeview; var company = me.page.fields_dict.company.get_value(); + if (!company) { + frappe.throw(__("Please set a Company")); + } frappe.call({ method: "erpnext.accounts.doctype.account.account.get_root_company", args: { diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 894ec5bdec..8834385135 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -72,7 +72,11 @@ def make_dimension_in_accounting_doctypes(doc): if doctype == "Budget": add_dimension_to_budget_doctype(df, doc) else: - create_custom_field(doctype, df) + meta = frappe.get_meta(doctype, cached=False) + fieldnames = [d.fieldname for d in meta.get("fields")] + + if df['fieldname'] not in fieldnames: + create_custom_field(doctype, df) count += 1 diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 353ff77223..8ca8b71ef8 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -19,10 +19,14 @@ "unlink_payment_on_cancellation_of_invoice", "unlink_advance_payment_on_cancelation_of_order", "book_asset_depreciation_entry_automatically", - "allow_cost_center_in_entry_of_bs_account", "add_taxes_from_item_tax_template", "automatically_fetch_payment_terms", + "deferred_accounting_settings_section", "automatically_process_deferred_accounting_entry", + "book_deferred_entries_based_on", + "column_break_18", + "book_deferred_entries_via_journal_entry", + "submit_journal_entries", "print_settings", "show_inclusive_tax_in_print", "column_break_12", @@ -108,12 +112,6 @@ "fieldtype": "Check", "label": "Book Asset Depreciation Entry Automatically" }, - { - "default": "0", - "fieldname": "allow_cost_center_in_entry_of_bs_account", - "fieldtype": "Check", - "label": "Allow Cost Center In Entry of Balance Sheet Account" - }, { "default": "1", "fieldname": "add_taxes_from_item_tax_template", @@ -189,13 +187,45 @@ "fieldname": "automatically_process_deferred_accounting_entry", "fieldtype": "Check", "label": "Automatically Process Deferred Accounting Entry" + }, + { + "fieldname": "deferred_accounting_settings_section", + "fieldtype": "Section Break", + "label": "Deferred Accounting Settings" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If this is unchecked direct GL Entries will be created to book Deferred Revenue/Expense", + "fieldname": "book_deferred_entries_via_journal_entry", + "fieldtype": "Check", + "label": "Book Deferred Entries Via Journal Entry" + }, + { + "default": "0", + "depends_on": "eval:doc.book_deferred_entries_via_journal_entry", + "description": "If this is unchecked Journal Entries will be saved in a Draft state and will have to be submitted manually", + "fieldname": "submit_journal_entries", + "fieldtype": "Check", + "label": "Submit Journal Entries" + }, + { + "default": "Days", + "description": "If \"Months\" is selected then fixed amount will be booked as deferred revenue or expense for each month irrespective of number of days in a month. Will be prorated if deferred revenue or expense is not booked for an entire month.", + "fieldname": "book_deferred_entries_based_on", + "fieldtype": "Select", + "label": "Book Deferred Entries Based On", + "options": "Days\nMonths" } ], "icon": "icon-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2019-12-19 16:58:17.395595", + "modified": "2020-06-22 20:13:26.043092", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 2473d715d0..5593466fc2 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -20,7 +20,6 @@ class AccountsSettings(Document): self.validate_stale_days() self.enable_payment_schedule_in_print() - self.enable_fields_for_cost_center_settings() def validate_stale_days(self): if not self.allow_stale and cint(self.stale_days) <= 0: @@ -33,8 +32,3 @@ class AccountsSettings(Document): for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"): make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check") make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check") - - def enable_fields_for_cost_center_settings(self): - show_field = 0 if cint(self.allow_cost_center_in_entry_of_bs_account) else 1 - for doctype in ("Sales Invoice", "Purchase Invoice", "Payment Entry"): - make_property_setter(doctype, "cost_center", "hidden", show_field, "Check") diff --git a/erpnext/accounts/doctype/cost_center/cost_center.js b/erpnext/accounts/doctype/cost_center/cost_center.js index 9e2f6eed3b..ee23b1be5c 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.js +++ b/erpnext/accounts/doctype/cost_center/cost_center.js @@ -14,7 +14,18 @@ frappe.ui.form.on('Cost Center', { is_group: 1 } } - }) + }); + + frm.set_query("cost_center", "distributed_cost_center", function() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + enable_distributed_cost_center: 0, + name: ['!=', frm.doc.name] + } + }; + }); }, refresh: function(frm) { if (!frm.is_new()) { @@ -60,8 +71,13 @@ frappe.ui.form.on('Cost Center', { "label": "Cost Center Number", "fieldname": "cost_center_number", "fieldtype": "Data", - "reqd": 1, "default": frm.doc.cost_center_number + }, + { + "label": __("Merge with existing"), + "fieldname": "merge", + "fieldtype": "Check", + "default": 0 } ], primary_action: function() { @@ -76,8 +92,9 @@ frappe.ui.form.on('Cost Center', { args: { docname: frm.doc.name, cost_center_name: data.cost_center_name, - cost_center_number: data.cost_center_number, - company: frm.doc.company + cost_center_number: cstr(data.cost_center_number), + company: frm.doc.company, + merge: data.merge }, callback: function(r) { frappe.dom.unfreeze(); diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json index 5013c92a32..e7fa954e01 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.json +++ b/erpnext/accounts/doctype/cost_center/cost_center.json @@ -16,6 +16,9 @@ "cb0", "is_group", "disabled", + "section_break_9", + "enable_distributed_cost_center", + "distributed_cost_center", "lft", "rgt", "old_parent" @@ -119,13 +122,31 @@ "fieldname": "disabled", "fieldtype": "Check", "label": "Disabled" + }, + { + "default": "0", + "fieldname": "enable_distributed_cost_center", + "fieldtype": "Check", + "label": "Enable Distributed Cost Center" + }, + { + "depends_on": "eval:doc.is_group==0", + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "depends_on": "enable_distributed_cost_center", + "fieldname": "distributed_cost_center", + "fieldtype": "Table", + "label": "Distributed Cost Center", + "options": "Distributed Cost Center" } ], "icon": "fa fa-money", "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-04-29 16:09:30.025214", + "modified": "2020-06-17 16:09:30.025214", "modified_by": "Administrator", "module": "Accounts", "name": "Cost Center", diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index 0294e78111..12094d4f98 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -19,6 +19,24 @@ class CostCenter(NestedSet): def validate(self): self.validate_mandatory() self.validate_parent_cost_center() + self.validate_distributed_cost_center() + + def validate_distributed_cost_center(self): + if cint(self.enable_distributed_cost_center): + 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")) + 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.")) + else: + self.distributed_cost_center = [] def validate_mandatory(self): if self.cost_center_name != self.company and not self.parent_cost_center: @@ -43,12 +61,15 @@ class CostCenter(NestedSet): return 1 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")) + 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")) if self.check_gle_exists(): frappe.throw(_("Cost Center with existing transactions can not be converted to group")) - else: - self.is_group = 1 - self.save() - return 1 + self.is_group = 1 + self.save() + return 1 def check_gle_exists(self): return frappe.db.get_value("GL Entry", {"cost_center": self.name}) @@ -57,6 +78,9 @@ class CostCenter(NestedSet): 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}) + 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 @@ -100,3 +124,7 @@ 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) + return next((True for x in value_list if x[0]), False) \ No newline at end of file diff --git a/erpnext/accounts/doctype/cost_center/test_cost_center.py b/erpnext/accounts/doctype/cost_center/test_cost_center.py index 8f23d90676..b5fc7e3b49 100644 --- a/erpnext/accounts/doctype/cost_center/test_cost_center.py +++ b/erpnext/accounts/doctype/cost_center/test_cost_center.py @@ -22,6 +22,33 @@ class TestCostCenter(unittest.TestCase): 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'}): + frappe.get_doc(test_records[0]).insert() + + 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 + } + ] + }) + + 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/report/purchase_order_items_to_be_billed/__init__.py b/erpnext/accounts/doctype/distributed_cost_center/__init__.py similarity index 100% rename from erpnext/accounts/report/purchase_order_items_to_be_billed/__init__.py rename to erpnext/accounts/doctype/distributed_cost_center/__init__.py diff --git a/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.json b/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.json new file mode 100644 index 0000000000..45b0e2df9b --- /dev/null +++ b/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "creation": "2020-03-19 12:34:01.500390", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "cost_center", + "percentage_allocation" + ], + "fields": [ + { + "fieldname": "cost_center", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Cost Center", + "options": "Cost Center", + "reqd": 1 + }, + { + "fieldname": "percentage_allocation", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Percentage Allocation", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-03-19 12:54:43.674655", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Distributed Cost Center", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py b/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py new file mode 100644 index 0000000000..48c589f0c0 --- /dev/null +++ b/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class DistributedCostCenter(Document): + pass diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 291aff3f5a..def9ed6803 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import flt, fmt_money, getdate, formatdate +from frappe.utils import flt, fmt_money, getdate, formatdate, cint from frappe.model.document import Document from frappe.model.naming import set_name_from_naming_options from frappe.model.meta import get_field_precision @@ -72,12 +72,6 @@ class GLEntry(Document): if not self.cost_center and self.voucher_type != 'Period Closing Voucher': frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.") .format(self.voucher_type, self.voucher_no, self.account)) - else: - from erpnext.accounts.utils import get_allow_cost_center_in_entry_of_bs_account - if not get_allow_cost_center_in_entry_of_bs_account() and self.cost_center: - self.cost_center = None - if self.project: - self.project = None def validate_dimensions_for_pl_and_bs(self): @@ -134,10 +128,17 @@ class GLEntry(Document): return self.cost_center_company[self.cost_center] + def _check_is_group(): + return cint(frappe.get_cached_value('Cost Center', self.cost_center, 'is_group')) + if self.cost_center and _get_cost_center_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)) + if self.cost_center and _check_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) diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js index 0fab8b70f4..db4f7c423f 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js @@ -188,14 +188,15 @@ frappe.ui.form.on('Invoice Discounting', { }, show_general_ledger: (frm) => { - if(frm.doc.docstatus===1) { + if(frm.doc.docstatus > 0) { cur_frm.add_custom_button(__('Accounting Ledger'), function() { frappe.route_options = { voucher_no: frm.doc.name, from_date: frm.doc.posting_date, - to_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), company: frm.doc.company, - group_by: "Group by Voucher (Consolidated)" + group_by: "Group by Voucher (Consolidated)", + show_cancelled_entries: frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); }, __("View")); diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 594b4d4a22..8083b21f75 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, json +import frappe, json, erpnext from frappe import _ from frappe.utils import flt, getdate, nowdate, add_days from erpnext.controllers.accounts_controller import AccountsController @@ -134,16 +134,19 @@ class InvoiceDiscounting(AccountsController): 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_charges_account, - "debit_in_account_currency": flt(self.bank_charges) + "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 }) @@ -151,6 +154,7 @@ class InvoiceDiscounting(AccountsController): 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", @@ -160,6 +164,7 @@ class InvoiceDiscounting(AccountsController): 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", @@ -177,13 +182,15 @@ class InvoiceDiscounting(AccountsController): 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) + "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()): @@ -193,6 +200,7 @@ class InvoiceDiscounting(AccountsController): 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", @@ -202,6 +210,7 @@ class InvoiceDiscounting(AccountsController): 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", diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.js b/erpnext/accounts/doctype/item_tax_template/item_tax_template.js index 42abdb809b..e921a0d949 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.js +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.js @@ -6,6 +6,18 @@ frappe.ui.form.on('Item Tax Template', { frm.set_query("tax_type", "taxes", function(doc) { return { filters: [ + ['Account', 'company', '=', frm.doc.company], + ['Account', 'is_group', '=', 0], + ['Account', 'account_type', 'in', ['Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation']] + ] + } + }); + }, + company: function (frm) { + frm.set_query("tax_type", "taxes", function(doc) { + return { + filters: [ + ['Account', 'company', '=', frm.doc.company], ['Account', 'is_group', '=', 0], ['Account', 'account_type', 'in', ['Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation']] ] diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json index f713cfc0ba..856c371ecf 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json @@ -1,168 +1,85 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:title", - "beta": 0, - "creation": "2018-11-22 22:45:00.370913", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:title", + "creation": "2018-11-22 22:45:00.370913", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "company", + "taxes" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Title", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "title", + "fieldtype": "Data", + "in_filter": 1, + "in_list_view": 1, + "label": "Title", + "no_copy": 1, + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "taxes", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Tax Rates", - "length": 0, - "no_copy": 0, - "options": "Item Tax Template Detail", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Tax Rates", + "options": "Item Tax Template Detail", + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-12-21 23:51:16.328340", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Item Tax Template", - "name_case": "", - "owner": "Administrator", + ], + "modified": "2020-06-18 20:27:42.615842", + "modified_by": "ahmad@havenir.com", + "module": "Accounts", + "name": "Item Tax Template", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 0 + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/item_tax_template/test_records.json b/erpnext/accounts/doctype/item_tax_template/test_records.json index db540e86aa..4d9537d4b8 100644 --- a/erpnext/accounts/doctype/item_tax_template/test_records.json +++ b/erpnext/accounts/doctype/item_tax_template/test_records.json @@ -2,6 +2,7 @@ { "doctype": "Item Tax Template", "title": "_Test Account Excise Duty @ 10", + "company": "_Test Company", "taxes": [ { "doctype": "Item Tax Template Detail", @@ -14,6 +15,7 @@ { "doctype": "Item Tax Template", "title": "_Test Account Excise Duty @ 12", + "company": "_Test Company", "taxes": [ { "doctype": "Item Tax Template Detail", @@ -26,6 +28,7 @@ { "doctype": "Item Tax Template", "title": "_Test Account Excise Duty @ 15", + "company": "_Test Company", "taxes": [ { "doctype": "Item Tax Template Detail", @@ -38,6 +41,7 @@ { "doctype": "Item Tax Template", "title": "_Test Account Excise Duty @ 20", + "company": "_Test Company", "taxes": [ { "doctype": "Item Tax Template Detail", @@ -50,6 +54,7 @@ { "doctype": "Item Tax Template", "title": "_Test Item Tax Template 1", + "company": "_Test Company", "taxes": [ { "doctype": "Item Tax Template Detail", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 9a832e3c1f..a09face791 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -13,15 +13,16 @@ frappe.ui.form.on("Journal Entry", { refresh: function(frm) { erpnext.toggle_naming_series(); - if(frm.doc.docstatus==1) { + if(frm.doc.docstatus > 0) { frm.add_custom_button(__('Ledger'), function() { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.posting_date, - "to_date": frm.doc.posting_date, + "to_date": moment(frm.doc.modified).format('YYYY-MM-DD'), "company": frm.doc.company, "finance_book": frm.doc.finance_book, - "group_by_voucher": 0 + "group_by": '', + "show_cancelled_entries": frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); }, __('View')); @@ -278,7 +279,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ // payroll entry if(jvd.reference_type==="Payroll Entry") { return { - query: "erpnext.hr.doctype.payroll_entry.payroll_entry.get_payroll_entries_for_jv", + query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.get_payroll_entries_for_jv", }; } diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 9d5063929f..4573c50134 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -83,7 +83,7 @@ "label": "Entry Type", "oldfieldname": "voucher_type", "oldfieldtype": "Select", - "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation", + "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense", "reqd": 1, "search_index": 1 }, @@ -191,6 +191,7 @@ { "fieldname": "total_debit", "fieldtype": "Currency", + "in_list_view": 1, "label": "Total Debit", "no_copy": 1, "oldfieldname": "total_debit", @@ -252,7 +253,6 @@ "fieldname": "total_amount", "fieldtype": "Currency", "hidden": 1, - "in_list_view": 1, "label": "Total Amount", "no_copy": 1, "options": "total_amount_currency", @@ -503,7 +503,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2020-04-29 10:55:28.240916", + "modified": "2020-06-02 18:15:46.955697", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 41922a2a69..cfdae936a4 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -10,6 +10,7 @@ from erpnext.accounts.utils import get_balance_on, get_account_currency from erpnext.accounts.party import get_party_account from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting +from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts from six import string_types, iteritems @@ -54,7 +55,7 @@ class JournalEntry(AccountsController): def on_cancel(self): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries - from erpnext.hr.doctype.salary_slip.salary_slip import unlink_ref_doc_from_salary_slip + 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') @@ -265,7 +266,10 @@ class JournalEntry(AccountsController): # set totals if not d.reference_name in self.reference_totals: self.reference_totals[d.reference_name] = 0.0 - self.reference_totals[d.reference_name] += flt(d.get(dr_or_cr)) + + 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 @@ -277,10 +281,16 @@ class JournalEntry(AccountsController): # check if party and account match if d.reference_type in ("Sales Invoice", "Purchase Invoice"): - if d.reference_type == "Sales Invoice": - party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or 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) else: - party_account = against_voucher[1] + if d.reference_type == "Sales Invoice": + party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1] + else: + party_account = against_voucher[1] + if (against_voucher[0] != d.party or party_account != d.account): 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], @@ -513,14 +523,20 @@ class JournalEntry(AccountsController): "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' + else: + update_outstanding = 'Yes' + if gl_map: - make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj) + make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding) def get_balance(self): if not self.get('accounts'): @@ -824,6 +840,7 @@ def get_opening_accounts(company): return [{"account": a, "balance": get_balance_on(a)} for a in accounts] +@frappe.whitelist() def get_against_jv(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select jv.name, jv.posting_date, jv.user_remark from `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 6996c775b3..23ad1eef14 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -204,11 +204,8 @@ class TestJournalEntry(unittest.TestCase): self.assertEqual(jv.inter_company_journal_entry_reference, "") self.assertEqual(jv1.inter_company_journal_entry_reference, "") - def test_jv_for_enable_allow_cost_center_in_entry_of_bs_account(self): + def test_jv_with_cost_centre(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 1 - accounts_settings.save() 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) @@ -237,15 +234,45 @@ class TestJournalEntry(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() + def test_jv_with_project(self): + from erpnext.projects.doctype.project.test_project import make_project + project = make_project({ + 'project_name': 'Journal Entry Project', + 'project_template_name': 'Test Project Template', + 'start_date': '2020-01-01' + }) - def test_jv_account_and_party_balance_for_enable_allow_cost_center_in_entry_of_bs_account(self): + jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False) + for d in jv.accounts: + d.project = project.project_name + jv.voucher_type = "Bank Entry" + jv.multi_currency = 0 + jv.cheque_no = "112233" + jv.cheque_date = nowdate() + jv.insert() + jv.submit() + + expected_values = { + "_Test Cash - _TC": { + "project": project.project_name + }, + "_Test Bank - _TC": { + "project": project.project_name + } + } + + 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) + + self.assertTrue(gl_entries) + + for gle in gl_entries: + self.assertEqual(expected_values[gle.account]["project"], gle.project) + + 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 - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 1 - accounts_settings.save() 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) @@ -261,9 +288,6 @@ class TestJournalEntry(unittest.TestCase): account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center) self.assertEqual(expected_account_balance, account_balance) - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - 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" diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 26c84a6398..774159d691 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -18,6 +18,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "currency_section", "account_currency", "column_break_10", @@ -32,7 +33,7 @@ "reference_type", "reference_name", "reference_due_date", - "project", + "reference_detail_no", "col_break3", "is_advance", "user_remark", @@ -268,12 +269,18 @@ "fieldtype": "Link", "label": "Bank Account", "options": "Bank Account" + }, + { + "fieldname": "reference_detail_no", + "fieldtype": "Data", + "hidden": 1, + "label": "Reference Detail No" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-25 01:47:49.060128", + "modified": "2020-06-24 14:06:54.833738", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 4d8da37efe..699eb08e17 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -11,21 +11,9 @@ frappe.ui.form.on('Opening Invoice Creation Tool', { }; }); - frm.set_query('cost_center', 'invoices', function(doc, cdt, cdn) { - return { - filters: { - 'company': doc.company - } - }; - }); - - frm.set_query('cost_center', function(doc) { - return { - filters: { - 'company': doc.company - } - }; - }); + if (frm.doc.company) { + frm.trigger('setup_company_filters'); + } }, refresh: function(frm) { @@ -51,19 +39,50 @@ frappe.ui.form.on('Opening Invoice Creation Tool', { }); }, - company: function(frm) { - frappe.call({ - method: 'erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool.get_temporary_opening_account', - args: { - company: frm.doc.company - }, - callback: (r) => { - if (r.message) { - frm.doc.__onload.temporary_opening_account = r.message; - frm.trigger('update_invoice_table'); + setup_company_filters: function(frm) { + frm.set_query('cost_center', 'invoices', function(doc, cdt, cdn) { + return { + filters: { + 'company': doc.company + } + }; + }); + + frm.set_query('cost_center', function(doc) { + return { + filters: { + 'company': doc.company + } + }; + }); + + frm.set_query('temporary_opening_account', 'invoices', function(doc, cdt, cdn) { + return { + filters: { + 'company': doc.company } } - }) + }); + }, + + company: function(frm) { + if (frm.doc.company) { + + frm.trigger('setup_company_filters'); + + frappe.call({ + method: 'erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool.get_temporary_opening_account', + args: { + company: frm.doc.company + }, + callback: (r) => { + if (r.message) { + frm.doc.__onload.temporary_opening_account = r.message; + frm.trigger('update_invoice_table'); + } + } + }) + } }, invoice_type: function(frm) { 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 54464e71c4..a53417eedf 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 @@ -68,6 +68,9 @@ class OpeningInvoiceCreationTool(Document): if not self.company: frappe.throw(_("Please select the Company")) + company_details = frappe.get_cached_value('Company', self.company, + ["default_currency", "default_letter_head"], as_dict=1) or {} + for row in self.invoices: if not row.qty: row.qty = 1.0 @@ -99,6 +102,12 @@ class OpeningInvoiceCreationTool(Document): if not args: continue + if company_details: + args.update({ + "currency": company_details.get("default_currency"), + "letter_head": company_details.get("default_letter_head") + }) + doc = frappe.get_doc(args).insert() doc.submit() names.append(doc.name) @@ -172,8 +181,7 @@ class OpeningInvoiceCreationTool(Document): "due_date": row.due_date, "posting_date": row.posting_date, frappe.scrub(party_type): row.party, - "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", - "currency": frappe.get_cached_value('Company', self.company, "default_currency") + "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice" }) accounting_dimension = get_accounting_dimensions() diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index a378a51cdf..42c9fdeba4 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -172,8 +172,8 @@ frappe.ui.form.on('Payment Entry', { frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency); frm.toggle_display("base_received_amount", ( - frm.doc.paid_to_account_currency != company_currency - && frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency + frm.doc.paid_to_account_currency != company_currency + && frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency && frm.doc.base_paid_amount != frm.doc.base_received_amount )); @@ -234,14 +234,15 @@ frappe.ui.form.on('Payment Entry', { }, show_general_ledger: function(frm) { - if(frm.doc.docstatus==1) { + if(frm.doc.docstatus > 0) { frm.add_custom_button(__('Ledger'), function() { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.posting_date, - "to_date": frm.doc.posting_date, + "to_date": moment(frm.doc.modified).format('YYYY-MM-DD'), "company": frm.doc.company, - group_by: "" + "group_by": "", + "show_cancelled_entries": frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); }, "fa fa-table"); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index d2245d6a6d..1cecab74ef 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext, json from frappe import _, scrub, ValidationError from frappe.utils import flt, comma_or, nowdate, getdate -from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on, get_allow_cost_center_in_entry_of_bs_account +from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on from erpnext.accounts.party import get_party_account from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.setup.utils import get_exchange_rate @@ -319,7 +319,7 @@ class PaymentEntry(AccountsController): invoice_payment_amount_map.setdefault(key, 0.0) invoice_payment_amount_map[key] += reference.allocated_amount - if not invoice_paid_amount_map.get(reference.reference_name): + if not invoice_paid_amount_map.get(key): payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name}, fields=['paid_amount', 'payment_amount', 'payment_term']) for term in payment_schedule: @@ -332,12 +332,14 @@ class PaymentEntry(AccountsController): frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) else: - outstanding = invoice_paid_amount_map.get(key)['outstanding'] + outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding')) + if amount > outstanding: frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0])) - frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s - WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) + if amount and outstanding: + frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s + WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) def set_status(self): if self.docstatus == 2: @@ -451,6 +453,8 @@ class PaymentEntry(AccountsController): frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) def set_remarks(self): + if self.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)] @@ -654,7 +658,7 @@ def get_outstanding_reference_documents(args): .format(frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])) # Add cost center condition - if args.get("cost_center") and get_allow_cost_center_in_entry_of_bs_account(): + if args.get("cost_center"): condition += " and cost_center='%s'" % args.get("cost_center") date_fields_dict = { @@ -1025,14 +1029,14 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= if bank_amount: received_amount = bank_amount else: - received_amount = paid_amount * doc.conversion_rate + received_amount = paid_amount * doc.get('conversion_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.conversion_rate + paid_amount = received_amount * doc.get('conversion_rate', 1) pe = frappe.new_doc("Payment Entry") pe.payment_type = payment_type @@ -1091,17 +1095,20 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount): references = [] for payment_term in payment_schedule: - 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': flt(payment_term.payment_amount - payment_term.paid_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 + }) return references diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 8bb741f0b2..772fc1a252 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -460,11 +460,8 @@ class TestPaymentEntry(unittest.TestCase): outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) self.assertEqual(outstanding_amount, 0) - def test_payment_entry_against_sales_invoice_for_enable_allow_cost_center_in_entry_of_bs_account(self): + def test_payment_entry_against_sales_invoice_with_cost_centre(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 1 - accounts_settings.save() cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") @@ -499,39 +496,8 @@ class TestPaymentEntry(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - - def test_payment_entry_against_sales_invoice_for_disable_allow_cost_center_in_entry_of_bs_account(self): - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - si = create_sales_invoice(debit_to="Debtors - _TC") - - pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") - - pe.reference_no = "112211-2" - pe.reference_date = nowdate() - pe.paid_to = "_Test Bank - _TC" - pe.paid_amount = si.grand_total - pe.insert() - pe.submit() - - 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) - - self.assertTrue(gl_entries) - - for gle in gl_entries: - self.assertEqual(gle.cost_center, None) - - def test_payment_entry_against_purchase_invoice_for_enable_allow_cost_center_in_entry_of_bs_account(self): + def test_payment_entry_against_purchase_invoice_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 1 - accounts_settings.save() cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") @@ -566,40 +532,9 @@ class TestPaymentEntry(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - - def test_payment_entry_against_purchase_invoice_for_disable_allow_cost_center_in_entry_of_bs_account(self): - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - pi = make_purchase_invoice(credit_to="Creditors - _TC") - - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") - - pe.reference_no = "112222-2" - pe.reference_date = nowdate() - pe.paid_from = "_Test Bank - _TC" - pe.paid_amount = pi.grand_total - pe.insert() - pe.submit() - - 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) - - self.assertTrue(gl_entries) - - for gle in gl_entries: - self.assertEqual(gle.cost_center, None) - - def test_payment_entry_account_and_party_balance_for_enable_allow_cost_center_in_entry_of_bs_account(self): + 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 - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 1 - accounts_settings.save() cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") @@ -630,9 +565,6 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(expected_party_balance, party_balance) self.assertEqual(expected_party_account_balance, party_account_balance) - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - def create_payment_terms_template(): create_payment_term('Basic Amount Receivable') @@ -665,4 +597,4 @@ def create_payment_term(name): frappe.get_doc({ 'doctype': 'Payment Term', 'payment_term_name': name - }).insert() \ No newline at end of file + }).insert() diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py index 7ecdc41d03..4702e58cef 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.py +++ b/erpnext/accounts/doctype/payment_order/payment_order.py @@ -26,6 +26,7 @@ class PaymentOrder(Document): for d in self.references: frappe.db.set_value(self.payment_order_type, d.get(frappe.scrub(self.payment_order_type)), ref_field, status) +@frappe.whitelist() def get_mop_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" select mode_of_payment from `tabPayment Order Reference` where parent = %(parent)s and mode_of_payment like %(txt)s @@ -36,6 +37,7 @@ def get_mop_query(doctype, txt, searchfield, start, page_len, filters): 'txt': "%%%s%%" % txt }) +@frappe.whitelist() def get_supplier_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" select supplier from `tabPayment Order Reference` where parent = %(parent)s and supplier like %(txt)s and @@ -86,4 +88,4 @@ def make_journal_entry(doc, supplier, mode_of_payment=None): je.flags.ignore_mandatory = True je.save() - frappe.msgprint(_("{0} {1} created").format(je.doctype, je.name)) \ No newline at end of file + frappe.msgprint(_("{0} {1} created").format(je.doctype, je.name)) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 3080496186..35d8d34c51 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -101,10 +101,10 @@ class PaymentReconciliation(Document): Having amount > 0 """.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)), + 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)), { 'party': self.party, 'party_type': self.party_type, @@ -170,7 +170,7 @@ class PaymentReconciliation(Document): reconcile_against_document(lst) if dr_or_cr_notes: - reconcile_dr_cr_note(dr_or_cr_notes) + reconcile_dr_cr_note(dr_or_cr_notes, self.company) msgprint(_("Successfully Reconciled")) self.get_unreconciled_entries() @@ -261,7 +261,7 @@ class PaymentReconciliation(Document): return cond -def reconcile_dr_cr_note(dr_cr_notes): +def reconcile_dr_cr_note(dr_cr_notes, company): for d in dr_cr_notes: voucher_type = ('Credit Note' if d.voucher_type == 'Sales Invoice' else 'Debit Note') @@ -273,6 +273,7 @@ def reconcile_dr_cr_note(dr_cr_notes): "doctype": "Journal Entry", "voucher_type": voucher_type, "posting_date": today(), + "company": company, "accounts": [ { 'account': d.account, @@ -280,7 +281,8 @@ def reconcile_dr_cr_note(dr_cr_notes): 'party_type': d.party_type, d.dr_or_cr: abs(d.allocated_amount), 'reference_type': d.against_voucher_type, - 'reference_name': d.against_voucher + 'reference_name': d.against_voucher, + 'cost_center': erpnext.get_default_cost_center(company) }, { 'account': d.account, @@ -289,7 +291,8 @@ def reconcile_dr_cr_note(dr_cr_notes): reconcile_dr_or_cr: (abs(d.allocated_amount) if abs(d.unadjusted_amount) > abs(d.allocated_amount) else abs(d.unadjusted_amount)), 'reference_type': d.voucher_type, - 'reference_name': d.voucher_no + 'reference_name': d.voucher_no, + 'cost_center': erpnext.get_default_cost_center(company) } ] }) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 7508683c08..eef6be1a7a 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -349,9 +349,10 @@ "read_only": 1 } ], + "in_create": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-08 10:23:02.815237", + "modified": "2020-05-29 17:38:49.392713", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 68aeb6d1d6..287e00f70f 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -69,7 +69,7 @@ class PaymentRequest(Document): elif self.payment_request_type == 'Inward': self.db_set('status', 'Requested') - send_mail = self.payment_gateway_validation() + 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") \ diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js index 87e02fef1b..e923d4ed5e 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js @@ -25,9 +25,10 @@ frappe.ui.form.on('Period Closing Voucher', { frappe.route_options = { "voucher_no": frm.doc.name, "from_date": frm.doc.posting_date, - "to_date": frm.doc.posting_date, + "to_date": moment(frm.doc.modified).format('YYYY-MM-DD'), "company": frm.doc.company, - group_by_voucher: 0 + "group_by": "", + "show_cancelled_entries": frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); }, "fa fa-table"); diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 4f17e9f995..f1869671ae 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -115,6 +115,7 @@ def get_item_groups(pos_profile): def get_series(): return frappe.get_meta("Sales Invoice").get_field("naming_series").options or "" +@frappe.whitelist() 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') diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 4d9053a55b..d4d83af1ed 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -17,6 +17,8 @@ from six import string_types 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() @@ -47,6 +49,13 @@ class PricingRule(Document): if tocheck and not self.get(tocheck): 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) + 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))) + + 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) @@ -80,13 +89,27 @@ class PricingRule(Document): for f in options: if not f: continue - f = frappe.scrub(f) - if f!=fieldname: - self.set(f, None) + scrubbed_f = frappe.scrub(f) + + if logic_field == 'apply_on': + apply_on_f = apply_on_dict.get(f, f) + else: + apply_on_f = scrubbed_f + + if scrubbed_f != fieldname: + self.set(apply_on_f, None) if self.mixed_conditions and self.get("same_item"): self.same_item = 0 + 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]) + + for other_field in cleanup_other_fields: + self.set(other_field, None) + def validate_rate_or_discount(self): for field in ["Rate"]: if flt(self.get(frappe.scrub(field))) < 0: @@ -409,6 +432,7 @@ def make_pricing_rule(doctype, docname): return doc +@frappe.whitelist() def get_item_uoms(doctype, txt, searchfield, start, page_len, filters): items = [filters.get('value')] if filters.get('apply_on') != 'Item Code': @@ -419,4 +443,4 @@ def get_item_uoms(doctype, txt, searchfield, start, page_len, filters): return frappe.get_all('UOM Conversion Detail', filters = {'parent': ('in', items), 'uom': ("like", "{0}%".format(txt))}, - fields = ["distinct uom"], as_list=1) \ No newline at end of file + 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 2da71dfd0e..2bf0b72563 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -385,6 +385,50 @@ class TestPricingRule(unittest.TestCase): so.load_from_db() self.assertEqual(so.items[1].is_free_item, 1) 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') + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Cumulative Pricing Rule", + "apply_on": "Item Code", + "currency": "USD", + "items": [{ + "item_code": "_Test Item", + }], + "is_cumulative": 1, + "selling": 1, + "applicable_for": "Customer", + "customer": "_Test Customer", + "rate_or_discount": "Discount Percentage", + "rate": 0, + "min_amt": 0, + "max_amt": 10000, + "discount_percentage": 17.5, + "price_or_product_discount": "Price", + "company": "_Test Company", + "valid_from": 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() + }) + details = get_item_details(args) + + self.assertTrue(details) def make_pricing_rule(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index cb05481df5..ad983830f3 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -319,7 +319,9 @@ def apply_internal_priority(pricing_rules, field_set, args): filtered_rules = [] for field in field_set: if args.get(field): - filtered_rules = filter(lambda x: x[field]==args[field], pricing_rules) + # 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 return filtered_rules or pricing_rules @@ -366,8 +368,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=[]): sum_qty, sum_amt = [0, 0] doctype = doc.get('parenttype') or doc.doctype - date_field = ('transaction_date' - if doc.get('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')) diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js index 975c60cf91..2800c195ce 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js @@ -10,6 +10,18 @@ frappe.ui.form.on('Process Deferred Accounting', { } }; }); + + if (frm.doc.company) { + frm.set_query("account", function() { + return { + filters: { + 'company': frm.doc.company, + 'root_type': 'Liability', + 'is_group': 0 + } + }; + }); + } }, validate: function() { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 98ba5c72ae..eb1ccd95af 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -26,6 +26,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "supplier_invoice_details", "bill_no", "column_break_15", @@ -42,6 +43,8 @@ "col_break_address", "shipping_address", "shipping_address_display", + "billing_address", + "billing_address_display", "currency_and_price_list", "currency", "conversion_rate", @@ -168,7 +171,9 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "naming_series", @@ -180,7 +185,9 @@ "options": "ACC-PINV-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier", @@ -192,7 +199,9 @@ "options": "Supplier", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -204,7 +213,9 @@ "label": "Supplier Name", "oldfieldname": "supplier_name", "oldfieldtype": "Data", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "supplier.tax_id", @@ -212,21 +223,27 @@ "fieldtype": "Read Only", "label": "Tax Id", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "due_date", "fieldtype": "Date", "label": "Due Date", "oldfieldname": "due_date", - "oldfieldtype": "Date" + "oldfieldtype": "Date", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "is_paid", "fieldtype": "Check", "label": "Is Paid", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -234,19 +251,25 @@ "fieldtype": "Check", "label": "Is Return (Debit Note)", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply Tax Withholding Amount", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -256,13 +279,17 @@ "label": "Company", "options": "Company", "print_hide": 1, - "remember_last_selected_value": 1 + "remember_last_selected_value": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 }, { "default": "Today", @@ -274,7 +301,9 @@ "oldfieldtype": "Date", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "posting_time", @@ -283,6 +312,8 @@ "no_copy": 1, "print_hide": 1, "print_width": "100px", + "show_days": 1, + "show_seconds": 1, "width": "100px" }, { @@ -291,7 +322,9 @@ "fieldname": "set_posting_time", "fieldtype": "Check", "label": "Edit Posting Date and Time", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -303,44 +336,58 @@ "oldfieldtype": "Link", "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:doc.on_hold", "fieldname": "sb_14", "fieldtype": "Section Break", - "label": "Hold Invoice" + "label": "Hold Invoice", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "on_hold", "fieldtype": "Check", - "label": "Hold Invoice" + "label": "Hold Invoice", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.on_hold", "description": "Once set, this invoice will be on hold till the set date", "fieldname": "release_date", "fieldtype": "Date", - "label": "Release Date" + "label": "Release Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cb_17", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.on_hold", "fieldname": "hold_comment", "fieldtype": "Small Text", - "label": "Reason For Putting On Hold" + "label": "Reason For Putting On Hold", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "bill_no", "fieldname": "supplier_invoice_details", "fieldtype": "Section Break", - "label": "Supplier Invoice Details" + "label": "Supplier Invoice Details", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bill_no", @@ -348,11 +395,15 @@ "label": "Supplier Invoice No", "oldfieldname": "bill_no", "oldfieldtype": "Data", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_15", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bill_date", @@ -360,13 +411,17 @@ "label": "Supplier Invoice Date", "oldfieldname": "bill_date", "oldfieldtype": "Date", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "return_against", "fieldname": "returns", "fieldtype": "Section Break", - "label": "Returns" + "label": "Returns", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "return_against", @@ -376,26 +431,34 @@ "no_copy": 1, "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact" + "label": "Address and Contact", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier_address", "fieldtype": "Link", "label": "Select Supplier Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_person", @@ -403,51 +466,67 @@ "in_global_search": 1, "label": "Contact Person", "options": "Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "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", "fieldtype": "Small Text", "label": "Contact Email", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_address", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address", "fieldtype": "Link", "label": "Select Shipping Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1 + "read_only": 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", @@ -456,7 +535,9 @@ "oldfieldname": "currency", "oldfieldtype": "Select", "options": "Currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "conversion_rate", @@ -465,18 +546,24 @@ "oldfieldname": "conversion_rate", "oldfieldtype": "Currency", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_currency", @@ -484,14 +571,18 @@ "label": "Price List Currency", "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "plc_conversion_rate", "fieldtype": "Float", "label": "Price List Exchange Rate", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -500,11 +591,15 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -512,7 +607,9 @@ "fieldtype": "Link", "label": "Set Accepted Warehouse", "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "update_stock", @@ -522,11 +619,15 @@ "label": "Rejected Warehouse", "no_copy": 1, "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -534,7 +635,9 @@ "fieldtype": "Select", "label": "Raw Materials Supplied", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_subcontracted==\"Yes\"", @@ -545,25 +648,33 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", + "show_days": 1, + "show_seconds": 1, "width": "50px" }, { "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 }, { "default": "0", "fieldname": "update_stock", "fieldtype": "Check", "label": "Update Stock", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "show_days": 1, + "show_seconds": 1 }, { "allow_bulk_edit": 1, @@ -573,42 +684,56 @@ "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Purchase Invoice Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "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 }, { "collapsible_depends_on": "supplied_items", "fieldname": "raw_materials_supplied", "fieldtype": "Section Break", - "label": "Raw Materials Supplied" + "label": "Raw Materials Supplied", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplied_items", "fieldtype": "Table", "label": "Supplied Items", "options": "Purchase Receipt Item Supplied", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_26", - "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", @@ -616,7 +741,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", @@ -626,18 +753,24 @@ "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_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", @@ -647,42 +780,56 @@ "oldfieldtype": "Currency", "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", "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_49", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_rule", "fieldtype": "Link", "label": "Shipping Rule", "options": "Shipping Rule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_51", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges", @@ -691,7 +838,9 @@ "oldfieldname": "purchase_other_charges", "oldfieldtype": "Link", "options": "Purchase Taxes and Charges Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", @@ -699,13 +848,17 @@ "label": "Purchase Taxes and Charges", "oldfieldname": "purchase_tax_details", "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges" + "options": "Purchase 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", @@ -714,13 +867,17 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_added", @@ -730,7 +887,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_deducted", @@ -740,7 +899,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -750,11 +911,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_40", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_added", @@ -764,7 +929,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_deducted", @@ -774,7 +941,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -782,14 +951,18 @@ "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" + "label": "Additional Discount", + "show_days": 1, + "show_seconds": 1 }, { "default": "Grand Total", @@ -797,7 +970,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", @@ -805,28 +980,38 @@ "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": "section_break_49", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_grand_total", @@ -836,7 +1021,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_rounding_adjustment", @@ -845,7 +1032,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -855,7 +1044,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_in_words", @@ -864,13 +1055,17 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break8", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -881,7 +1076,9 @@ "oldfieldname": "grand_total_import", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rounding_adjustment", @@ -890,7 +1087,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -900,7 +1099,9 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "in_words", @@ -909,7 +1110,9 @@ "oldfieldname": "in_words_import", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_advance", @@ -920,7 +1123,9 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "outstanding_amount", @@ -931,14 +1136,18 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "depends_on": "grand_total", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total" + "label": "Disable Rounded Total", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -946,30 +1155,40 @@ "depends_on": "eval:doc.is_paid===1||(doc.advances && doc.advances.length>0)", "fieldname": "payments_section", "fieldtype": "Section Break", - "label": "Payments" + "label": "Payments", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", "options": "Mode of Payment", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cash_bank_account", "fieldtype": "Link", "label": "Cash/Bank Account", - "options": "Account" + "options": "Account", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "clearance_date", "fieldtype": "Date", "hidden": 1, - "label": "Clearance Date" + "label": "Clearance Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_br_payments", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "is_paid", @@ -978,7 +1197,9 @@ "label": "Paid Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_paid_amount", @@ -987,7 +1208,9 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -995,7 +1218,9 @@ "depends_on": "grand_total", "fieldname": "write_off", "fieldtype": "Section Break", - "label": "Write Off" + "label": "Write Off", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "write_off_amount", @@ -1003,7 +1228,9 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_write_off_amount", @@ -1012,11 +1239,15 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_61", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1024,7 +1255,9 @@ "fieldtype": "Link", "label": "Write Off Account", "options": "Account", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1032,7 +1265,9 @@ "fieldtype": "Link", "label": "Write Off Cost Center", "options": "Cost Center", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1042,13 +1277,17 @@ "label": "Advance Payments", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "allocate_advances_automatically", "fieldtype": "Check", - "label": "Set Advances and Allocate (FIFO)" + "label": "Set Advances and Allocate (FIFO)", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.allocate_advances_automatically", @@ -1056,7 +1295,9 @@ "fieldtype": "Button", "label": "Get Advances Paid", "oldfieldtype": "Button", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "advances", @@ -1066,20 +1307,26 @@ "oldfieldname": "advance_allocation_details", "oldfieldtype": "Table", "options": "Purchase Invoice Advance", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:(!doc.is_return)", "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" + "options": "Payment Terms Template", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_schedule", @@ -1087,7 +1334,9 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -1095,25 +1344,33 @@ "fieldname": "terms_section_break", "fieldtype": "Section Break", "label": "Terms and Conditions", - "options": "fa fa-legal" + "options": "fa fa-legal", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tc_name", "fieldtype": "Link", "label": "Terms", "options": "Terms and Conditions", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", - "label": "Terms and Conditions1" + "label": "Terms and Conditions1", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "printing_settings", "fieldtype": "Section Break", - "label": "Printing Settings" + "label": "Printing Settings", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1121,7 +1378,9 @@ "fieldtype": "Link", "label": "Letter Head", "options": "Letter Head", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1129,11 +1388,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_112", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1145,14 +1408,18 @@ "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 }, { "collapsible": 1, @@ -1161,7 +1428,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": "credit_to", @@ -1172,7 +1441,9 @@ "options": "Account", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "party_account_currency", @@ -1182,7 +1453,9 @@ "no_copy": 1, "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -1192,7 +1465,9 @@ "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "against_expense_account", @@ -1202,11 +1477,15 @@ "no_copy": 1, "oldfieldname": "against_expense_account", "oldfieldtype": "Small Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_63", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Draft", @@ -1215,14 +1494,18 @@ "in_standard_filter": 1, "label": "Status", "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "inter_company_invoice_reference", "fieldtype": "Link", "label": "Inter Company Invoice Reference", "options": "Sales Invoice", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "remarks", @@ -1231,14 +1514,18 @@ "no_copy": 1, "oldfieldname": "remarks", "oldfieldtype": "Text", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", "label": "Subscription Section", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1247,7 +1534,9 @@ "fieldtype": "Date", "label": "From Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1256,11 +1545,15 @@ "fieldtype": "Date", "label": "To Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_114", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "auto_repeat", @@ -1269,24 +1562,32 @@ "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, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions " + "label": "Accounting Dimensions ", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -1294,7 +1595,15 @@ "fieldname": "is_internal_supplier", "fieldtype": "Check", "label": "Is Internal Supplier", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" }, { "fieldname": "tax_withholding_category", @@ -1302,14 +1611,32 @@ "hidden": 1, "label": "Tax Withholding Category", "options": "Tax Withholding Category", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Select Billing Address", + "options": "Address", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-04-18 13:05:25.199832", + "modified": "2020-06-13 22:26:30.800199", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index aa1d5b526c..7b1062f654 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -238,6 +238,12 @@ class PurchaseInvoice(BuyingController): 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"]: + frappe.msgprint(_('''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)))) + item.expense_account = warehouse_account[item.warehouse]["account"] else: # check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not @@ -247,10 +253,21 @@ class PurchaseInvoice(BuyingController): (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: + frappe.msgprint(_('''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)))) + 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: + frappe.msgprint(_('''Row {0}: Expense Head changed to {1} as no Purchase + Receipt is created against Item {2}. This is done to handle accounting for cases + when Purchase Receipt is created after Purchase Invoice'''.format( + item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code)))) + item.expense_account = stock_not_billed_account elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): @@ -421,6 +438,8 @@ class PurchaseInvoice(BuyingController): self.make_tax_gl_entries(gl_entries) + gl_entries = make_regional_gl_entries(gl_entries, self) + gl_entries = merge_similar_entries(gl_entries) self.make_payment_gl_entries(gl_entries) @@ -459,6 +478,7 @@ class PurchaseInvoice(BuyingController): 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) ) @@ -499,6 +519,7 @@ class PurchaseInvoice(BuyingController): "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)) @@ -508,6 +529,7 @@ class PurchaseInvoice(BuyingController): "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)) @@ -531,7 +553,7 @@ class PurchaseInvoice(BuyingController): "debit": warehouse_debit_amount, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "cost_center": item.cost_center, - "project": item.project + "project": item.project or self.project }, account_currency, item=item) ) @@ -544,7 +566,7 @@ class PurchaseInvoice(BuyingController): "cost_center": item.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "credit": flt(amount), - "project": item.project + "project": item.project or self.project }, item=item)) # sub-contracting warehouse @@ -557,6 +579,7 @@ class PurchaseInvoice(BuyingController): "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)) @@ -570,12 +593,26 @@ class PurchaseInvoice(BuyingController): else: amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) + auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) + + if auto_accounting_for_non_stock_items: + service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") + + if item.purchase_receipt: + # 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':service_received_but_not_billed_account}, ['name']) + + if expense_booked_in_pr: + expense_account = service_received_but_not_billed_account + gl_entries.append(self.get_gl_dict({ "account": expense_account, "against": self.supplier, "debit": amount, "cost_center": item.cost_center, - "project": item.project + "project": item.project or self.project }, account_currency, item=item)) # If asset is bought through this document and not linked to PR @@ -588,7 +625,7 @@ class PurchaseInvoice(BuyingController): "cost_center": item.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "credit": flt(item.landed_cost_voucher_amount), - "project": item.project + "project": item.project or self.project }, item=item)) gl_entries.append(self.get_gl_dict({ @@ -597,7 +634,7 @@ class PurchaseInvoice(BuyingController): "cost_center": item.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "debit": flt(item.landed_cost_voucher_amount), - "project": item.project + "project": item.project or self.project }, item=item)) # update gross amount of asset bought through this document @@ -623,7 +660,8 @@ class PurchaseInvoice(BuyingController): "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 + "cost_center": self.cost_center, + "project": item.project or self.project }, item=item) ) @@ -652,7 +690,8 @@ class PurchaseInvoice(BuyingController): "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 + "cost_center": item.cost_center, + "project": item.project or self.project }, item=item)) if item.item_tax_amount: @@ -662,6 +701,7 @@ class PurchaseInvoice(BuyingController): "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 @@ -678,7 +718,8 @@ class PurchaseInvoice(BuyingController): "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 + "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)): @@ -689,6 +730,7 @@ class PurchaseInvoice(BuyingController): "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) @@ -704,7 +746,7 @@ class PurchaseInvoice(BuyingController): "cost_center": item.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "credit": flt(item.landed_cost_voucher_amount), - "project": item.project + "project": item.project or self.project }, item=item)) gl_entries.append(self.get_gl_dict({ @@ -713,7 +755,7 @@ class PurchaseInvoice(BuyingController): "cost_center": item.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "debit": flt(item.landed_cost_voucher_amount), - "project": item.project + "project": item.project or self.project }, item=item)) # update gross amount of assets bought through this document @@ -748,7 +790,7 @@ class PurchaseInvoice(BuyingController): "debit": stock_adjustment_amt, "remarks": self.get("remarks") or _("Stock Adjustment"), "cost_center": item.cost_center, - "project": item.project + "project": item.project or self.project }, account_currency, item=item) ) @@ -840,7 +882,8 @@ class PurchaseInvoice(BuyingController): 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 + "cost_center": self.cost_center, + "project": self.project }, self.party_account_currency, item=self) ) @@ -872,7 +915,8 @@ class PurchaseInvoice(BuyingController): 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 + "cost_center": self.cost_center, + "project": self.project }, self.party_account_currency, item=self) ) gl_entries.append( @@ -1020,7 +1064,7 @@ class PurchaseInvoice(BuyingController): # calculate totals again after applying TDS self.calculate_taxes_and_totals() - + def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): if self.get('amended_from'): @@ -1066,6 +1110,10 @@ def get_list_context(context=None): }) 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 diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index 800ed921bd..86c2e408c0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -16,7 +16,7 @@ frappe.listview_settings['Purchase Invoice'] = { } else if(frappe.datetime.get_diff(doc.due_date) < 0) { return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"]; } else { - return [__("Unpaid"), "orange", "outstanding_amount,>,0|due,>=,Today"]; + return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"]; } } else if(cint(doc.is_return)) { return [__("Return"), "darkgrey", "is_return,=,Yes"]; @@ -24,4 +24,4 @@ frappe.listview_settings['Purchase Invoice'] = { return [__("Paid"), "green", "outstanding_amount,=,0"]; } } -}; +}; \ No newline at end of file diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 6170005061..9a666bf9f8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -7,14 +7,16 @@ import unittest import frappe, erpnext import frappe.model from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry -from frappe.utils import cint, flt, today, nowdate, add_days +from frappe.utils import cint, flt, today, nowdate, add_days, getdate import frappe.defaults from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \ test_records as pr_test_records, make_purchase_receipt, get_taxes from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.exceptions import InvalidCurrency from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction -from erpnext.accounts.doctype.account.test_account import get_inventory_account +from erpnext.projects.doctype.project.test_project import make_project +from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account +from erpnext.stock.doctype.item.test_item import create_item test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"] test_ignore = ["Serial No"] @@ -434,6 +436,8 @@ class TestPurchaseInvoice(unittest.TestCase): ) def test_total_purchase_cost_for_project(self): + make_project({'project_name':'_Test Project'}) + existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount) from `tabPurchase Invoice Item` where project = '_Test Project' and docstatus=1""") existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0 @@ -807,11 +811,8 @@ class TestPurchaseInvoice(unittest.TestCase): pi_doc = frappe.get_doc('Purchase Invoice', pi.name) self.assertEqual(pi_doc.outstanding_amount, 0) - def test_purchase_invoice_for_enable_allow_cost_center_in_entry_of_bs_account(self): + def test_purchase_invoice_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 1 - accounts_settings.save() cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") @@ -837,13 +838,7 @@ class TestPurchaseInvoice(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - - def test_purchase_invoice_for_disable_allow_cost_center_in_entry_of_bs_account(self): - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() + def test_purchase_invoice_without_cost_center(self): cost_center = "_Test Cost Center - _TC" pi = make_purchase_invoice(credit_to="Creditors - _TC") @@ -866,6 +861,104 @@ class TestPurchaseInvoice(unittest.TestCase): for gle in gl_entries: 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' + }) + + pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1) + pi.items[0].project = item_project.project_name + pi.project = project.project_name + + pi.submit() + + expected_values = { + "Creditors - _TC": { + "project": project.project_name + }, + "_Test Account Cost for Goods Sold - _TC": { + "project": item_project.project_name + } + } + + 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) + + self.assertTrue(gl_entries) + + for gle in gl_entries: + 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") + + 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() + + item = create_item("_Test Item for Deferred Accounting") + item.enable_deferred_expense = 1 + item.deferred_expense_account = deferred_account + item.save() + + pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True) + pi.set_posting_time = 1 + pi.posting_date = '2019-03-15' + 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" + pi.items[0].deferred_expense_account = deferred_account + 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.insert() + pda1.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 0.0, 33.85, "2019-01-31"], + [deferred_account, 33.85, 0.0, "2019-01-31"], + ["_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"] + ] + + 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) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.credit) + self.assertEqual(expected_gle[i][2], gle.debit) + self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + + acc_settings = frappe.get_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 unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index db20589144..02b4206544 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -189,6 +189,8 @@ { "fieldname": "customer_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "options": "fa fa-user" }, { @@ -197,6 +199,8 @@ "fieldname": "title", "fieldtype": "Data", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Title", "no_copy": 1, "print_hide": 1 @@ -205,6 +209,8 @@ "bold": 1, "fieldname": "naming_series", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Series", "no_copy": 1, "oldfieldname": "naming_series", @@ -218,6 +224,8 @@ "bold": 1, "fieldname": "customer", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Customer", "oldfieldname": "customer", @@ -232,6 +240,8 @@ "fetch_from": "customer.customer_name", "fieldname": "customer_name", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "in_global_search": 1, "label": "Customer Name", "oldfieldname": "customer_name", @@ -241,6 +251,8 @@ { "fieldname": "tax_id", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Tax Id", "print_hide": 1, "read_only": 1 @@ -248,6 +260,8 @@ { "fieldname": "project", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_global_search": 1, "label": "Project", "oldfieldname": "project_name", @@ -259,6 +273,8 @@ "default": "0", "fieldname": "is_pos", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Include Payment (POS)", "oldfieldname": "is_pos", "oldfieldtype": "Check", @@ -268,6 +284,8 @@ "depends_on": "is_pos", "fieldname": "pos_profile", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "POS Profile", "options": "POS Profile", "print_hide": 1 @@ -276,6 +294,8 @@ "fieldname": "offline_pos_name", "fieldtype": "Data", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Offline POS Name", "print_hide": 1, "read_only": 1 @@ -284,6 +304,8 @@ "default": "0", "fieldname": "is_return", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Is Return (Credit Note)", "no_copy": 1, "print_hide": 1 @@ -291,11 +313,15 @@ { "fieldname": "column_break1", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Column Break" }, { "fieldname": "company", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Company", "oldfieldname": "company", @@ -308,6 +334,8 @@ { "fieldname": "cost_center", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Cost Center", "options": "Cost Center" }, @@ -316,6 +344,8 @@ "default": "Today", "fieldname": "posting_date", "fieldtype": "Date", + "hide_days": 1, + "hide_seconds": 1, "label": "Date", "no_copy": 1, "oldfieldname": "posting_date", @@ -326,6 +356,8 @@ { "fieldname": "posting_time", "fieldtype": "Time", + "hide_days": 1, + "hide_seconds": 1, "label": "Posting Time", "no_copy": 1, "oldfieldname": "posting_time", @@ -337,12 +369,16 @@ "depends_on": "eval:doc.docstatus==0", "fieldname": "set_posting_time", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Edit Posting Date and Time", "print_hide": 1 }, { "fieldname": "due_date", "fieldtype": "Date", + "hide_days": 1, + "hide_seconds": 1, "label": "Payment Due Date", "no_copy": 1, "oldfieldname": "due_date", @@ -351,6 +387,8 @@ { "fieldname": "amended_from", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "label": "Amended From", "no_copy": 1, @@ -364,12 +402,16 @@ "depends_on": "return_against", "fieldname": "returns", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Returns" }, { "depends_on": "return_against", "fieldname": "return_against", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Return Against Sales Invoice", "no_copy": 1, "options": "Sales Invoice", @@ -379,13 +421,17 @@ }, { "fieldname": "column_break_21", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "depends_on": "eval: doc.is_return && doc.return_against", "fieldname": "update_billed_amount_in_sales_order", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Update Billed Amount in Sales Order" }, { @@ -393,35 +439,47 @@ "collapsible_depends_on": "po_no", "fieldname": "customer_po_details", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Customer PO Details" }, { "allow_on_submit": 1, "fieldname": "po_no", - "fieldtype": "Data", + "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Customer's Purchase Order", "no_copy": 1, "print_hide": 1 }, { "fieldname": "column_break_23", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "po_date", "fieldtype": "Date", + "hide_days": 1, + "hide_seconds": 1, "label": "Customer's Purchase Order Date" }, { "collapsible": 1, "fieldname": "address_and_contact", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Address and Contact" }, { "fieldname": "customer_address", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Customer Address", "options": "Address", "print_hide": 1 @@ -429,12 +487,16 @@ { "fieldname": "address_display", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Address", "read_only": 1 }, { "fieldname": "contact_person", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_global_search": 1, "label": "Contact Person", "options": "Contact", @@ -443,6 +505,8 @@ { "fieldname": "contact_display", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Contact", "read_only": 1 }, @@ -450,6 +514,8 @@ "fieldname": "contact_mobile", "fieldtype": "Small Text", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Mobile No", "read_only": 1 }, @@ -457,6 +523,8 @@ "fieldname": "contact_email", "fieldtype": "Data", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Contact Email", "options": "Email", "print_hide": 1, @@ -465,17 +533,23 @@ { "fieldname": "territory", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Territory", "options": "Territory", "print_hide": 1 }, { "fieldname": "col_break4", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "shipping_address_name", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Shipping Address Name", "options": "Address", "print_hide": 1 @@ -483,6 +557,8 @@ { "fieldname": "shipping_address", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Shipping Address", "print_hide": 1, "read_only": 1 @@ -490,6 +566,8 @@ { "fieldname": "company_address", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Company Address Name", "options": "Address", "print_hide": 1 @@ -498,6 +576,8 @@ "fieldname": "company_address_display", "fieldtype": "Small Text", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Company Address", "print_hide": 1, "read_only": 1 @@ -507,11 +587,15 @@ "depends_on": "customer", "fieldname": "currency_and_price_list", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Currency and Price List" }, { "fieldname": "currency", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Currency", "oldfieldname": "currency", "oldfieldtype": "Select", @@ -523,6 +607,8 @@ "description": "Rate at which Customer Currency is converted to customer's base currency", "fieldname": "conversion_rate", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Exchange Rate", "oldfieldname": "conversion_rate", "oldfieldtype": "Currency", @@ -533,11 +619,15 @@ { "fieldname": "column_break2", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "width": "50%" }, { "fieldname": "selling_price_list", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Price List", "oldfieldname": "price_list_name", "oldfieldtype": "Select", @@ -548,6 +638,8 @@ { "fieldname": "price_list_currency", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Price List Currency", "options": "Currency", "print_hide": 1, @@ -558,6 +650,8 @@ "description": "Rate at which Price list currency is converted to customer's base currency", "fieldname": "plc_conversion_rate", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Price List Exchange Rate", "precision": "9", "print_hide": 1, @@ -567,6 +661,8 @@ "default": "0", "fieldname": "ignore_pricing_rule", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, @@ -574,12 +670,16 @@ }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "depends_on": "update_stock", "fieldname": "set_warehouse", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Set Source Warehouse", "options": "Warehouse", "print_hide": 1 @@ -587,6 +687,8 @@ { "fieldname": "items_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Section Break", "options": "fa fa-shopping-cart" }, @@ -594,6 +696,8 @@ "default": "0", "fieldname": "update_stock", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Update Stock", "oldfieldname": "update_stock", "oldfieldtype": "Check", @@ -602,12 +706,16 @@ { "fieldname": "scan_barcode", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Scan Barcode" }, { "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Items", "oldfieldname": "entries", "oldfieldtype": "Table", @@ -617,11 +725,15 @@ { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Pricing Rules" }, { "fieldname": "pricing_rules", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", "read_only": 1 @@ -629,6 +741,8 @@ { "fieldname": "packing_list", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Packing List", "options": "fa fa-suitcase", "print_hide": 1 @@ -636,6 +750,8 @@ { "fieldname": "packed_items", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Packed Items", "options": "Packed Item", "print_hide": 1 @@ -643,6 +759,8 @@ { "fieldname": "product_bundle_help", "fieldtype": "HTML", + "hide_days": 1, + "hide_seconds": 1, "label": "Product Bundle Help", "print_hide": 1 }, @@ -651,11 +769,15 @@ "collapsible_depends_on": "eval:doc.total_billing_amount > 0", "fieldname": "time_sheet_list", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Time Sheet List" }, { "fieldname": "timesheets", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Time Sheets", "options": "Sales Invoice Timesheet", "print_hide": 1 @@ -664,23 +786,31 @@ "default": "0", "fieldname": "total_billing_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Billing Amount", "print_hide": 1, "read_only": 1 }, { "fieldname": "section_break_30", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "total_qty", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Quantity", "read_only": 1 }, { "fieldname": "base_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, @@ -689,6 +819,8 @@ { "fieldname": "base_net_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Net Total (Company Currency)", "oldfieldname": "net_total", "oldfieldtype": "Currency", @@ -699,11 +831,15 @@ }, { "fieldname": "column_break_32", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total", "options": "currency", "read_only": 1 @@ -711,6 +847,8 @@ { "fieldname": "net_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Net Total", "options": "currency", "print_hide": 1, @@ -719,6 +857,8 @@ { "fieldname": "total_net_weight", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Net Weight", "print_hide": 1, "read_only": 1 @@ -726,12 +866,16 @@ { "fieldname": "taxes_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Section Break", "options": "fa fa-money" }, { "fieldname": "taxes_and_charges", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Taxes and Charges Template", "oldfieldname": "charge", "oldfieldtype": "Link", @@ -740,11 +884,15 @@ }, { "fieldname": "column_break_38", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "shipping_rule", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Shipping Rule", "oldfieldtype": "Button", "options": "Shipping Rule", @@ -753,17 +901,23 @@ { "fieldname": "tax_category", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Tax Category", "options": "Tax Category", "print_hide": 1 }, { "fieldname": "section_break_40", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "taxes", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Taxes and Charges", "oldfieldname": "other_charges", "oldfieldtype": "Table", @@ -773,11 +927,15 @@ "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Tax Breakup" }, { "fieldname": "other_charges_calculation", "fieldtype": "Long Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -786,11 +944,15 @@ }, { "fieldname": "section_break_43", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Taxes and Charges (Company Currency)", "oldfieldname": "other_charges_total", "oldfieldtype": "Currency", @@ -800,11 +962,15 @@ }, { "fieldname": "column_break_47", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "total_taxes_and_charges", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, @@ -814,12 +980,16 @@ "collapsible": 1, "fieldname": "loyalty_points_redemption", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Loyalty Points Redemption" }, { "depends_on": "redeem_loyalty_points", "fieldname": "loyalty_points", "fieldtype": "Int", + "hide_days": 1, + "hide_seconds": 1, "label": "Loyalty Points", "no_copy": 1, "print_hide": 1 @@ -828,6 +998,8 @@ "depends_on": "redeem_loyalty_points", "fieldname": "loyalty_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Loyalty Amount", "no_copy": 1, "options": "Company:company:default_currency", @@ -838,18 +1010,24 @@ "default": "0", "fieldname": "redeem_loyalty_points", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Redeem Loyalty Points", "no_copy": 1, "print_hide": 1 }, { "fieldname": "column_break_77", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fetch_from": "customer.loyalty_program", "fieldname": "loyalty_program", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Loyalty Program", "no_copy": 1, "options": "Loyalty Program", @@ -860,6 +1038,8 @@ "depends_on": "redeem_loyalty_points", "fieldname": "loyalty_redemption_account", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Redemption Account", "no_copy": 1, "options": "Account" @@ -868,6 +1048,8 @@ "depends_on": "redeem_loyalty_points", "fieldname": "loyalty_redemption_cost_center", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Redemption Cost Center", "no_copy": 1, "options": "Cost Center" @@ -877,12 +1059,16 @@ "collapsible_depends_on": "discount_amount", "fieldname": "section_break_49", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Additional Discount" }, { "default": "Grand Total", "fieldname": "apply_discount_on", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", "print_hide": 1 @@ -890,6 +1076,8 @@ { "fieldname": "base_discount_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, @@ -897,17 +1085,23 @@ }, { "fieldname": "column_break_51", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Additional Discount Percentage", "print_hide": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Additional Discount Amount", "options": "currency", "print_hide": 1 @@ -915,6 +1109,8 @@ { "fieldname": "totals", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Section Break", "options": "fa fa-money", "print_hide": 1 @@ -922,6 +1118,8 @@ { "fieldname": "base_grand_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Grand Total (Company Currency)", "oldfieldname": "grand_total", "oldfieldtype": "Currency", @@ -933,6 +1131,8 @@ { "fieldname": "base_rounding_adjustment", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Rounding Adjustment (Company Currency)", "no_copy": 1, "options": "Company:company:default_currency", @@ -942,6 +1142,8 @@ { "fieldname": "base_rounded_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Rounded Total (Company Currency)", "oldfieldname": "rounded_total", "oldfieldtype": "Currency", @@ -953,6 +1155,8 @@ "description": "In Words will be visible once you save the Sales Invoice.", "fieldname": "base_in_words", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "In Words (Company Currency)", "oldfieldname": "in_words", "oldfieldtype": "Data", @@ -962,6 +1166,8 @@ { "fieldname": "column_break5", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Column Break", "print_hide": 1, "width": "50%" @@ -970,6 +1176,8 @@ "bold": 1, "fieldname": "grand_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Grand Total", "oldfieldname": "grand_total_export", @@ -981,6 +1189,8 @@ { "fieldname": "rounding_adjustment", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Rounding Adjustment", "no_copy": 1, "options": "currency", @@ -991,6 +1201,8 @@ "bold": 1, "fieldname": "rounded_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Rounded Total", "oldfieldname": "rounded_total_export", "oldfieldtype": "Currency", @@ -1000,6 +1212,8 @@ { "fieldname": "in_words", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "In Words", "oldfieldname": "in_words_export", "oldfieldtype": "Data", @@ -1009,6 +1223,8 @@ { "fieldname": "total_advance", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Advance", "oldfieldname": "total_advance", "oldfieldtype": "Currency", @@ -1019,6 +1235,8 @@ { "fieldname": "outstanding_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Outstanding Amount", "no_copy": 1, "oldfieldname": "outstanding_amount", @@ -1032,6 +1250,8 @@ "collapsible_depends_on": "advances", "fieldname": "advances_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Advance Payments", "oldfieldtype": "Section Break", "options": "fa fa-money", @@ -1041,18 +1261,24 @@ "default": "0", "fieldname": "allocate_advances_automatically", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Allocate Advances Automatically (FIFO)" }, { "depends_on": "eval:!doc.allocate_advances_automatically", "fieldname": "get_advances", "fieldtype": "Button", + "hide_days": 1, + "hide_seconds": 1, "label": "Get Advances Received", "options": "set_advances" }, { "fieldname": "advances", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Advances", "oldfieldname": "advance_adjustment_details", "oldfieldtype": "Table", @@ -1064,12 +1290,16 @@ "collapsible_depends_on": "eval:(!doc.is_pos && !doc.is_return)", "fieldname": "payment_schedule_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Payment Terms" }, { "depends_on": "eval:(!doc.is_pos && !doc.is_return)", "fieldname": "payment_terms_template", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Payment Terms Template", "no_copy": 1, "options": "Payment Terms Template", @@ -1079,6 +1309,8 @@ "depends_on": "eval:(!doc.is_pos && !doc.is_return)", "fieldname": "payment_schedule", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", @@ -1088,6 +1320,8 @@ "depends_on": "eval:doc.is_pos===1||(doc.advances && doc.advances.length>0)", "fieldname": "payments_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Payments", "options": "fa fa-money" }, @@ -1096,6 +1330,8 @@ "fieldname": "cash_bank_account", "fieldtype": "Link", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Cash/Bank Account", "oldfieldname": "cash_bank_account", "oldfieldtype": "Link", @@ -1106,17 +1342,23 @@ "depends_on": "eval:doc.is_pos===1", "fieldname": "payments", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Invoice Payment", "options": "Sales Invoice Payment", "print_hide": 1 }, { "fieldname": "section_break_84", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "base_paid_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Paid Amount (Company Currency)", "no_copy": 1, "options": "Company:company:default_currency", @@ -1125,12 +1367,16 @@ }, { "fieldname": "column_break_86", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "depends_on": "eval: doc.is_pos || doc.redeem_loyalty_points", "fieldname": "paid_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Paid Amount", "no_copy": 1, "oldfieldname": "paid_amount", @@ -1141,12 +1387,16 @@ }, { "fieldname": "section_break_88", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "depends_on": "is_pos", "fieldname": "base_change_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Base Change Amount (Company Currency)", "no_copy": 1, "options": "Company:company:default_currency", @@ -1155,12 +1405,16 @@ }, { "fieldname": "column_break_90", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "depends_on": "is_pos", "fieldname": "change_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Change Amount", "no_copy": 1, "options": "currency", @@ -1170,6 +1424,8 @@ "depends_on": "is_pos", "fieldname": "account_for_change_amount", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Account for Change Amount", "options": "Account", "print_hide": 1 @@ -1180,12 +1436,16 @@ "depends_on": "grand_total", "fieldname": "column_break4", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Write Off", "width": "50%" }, { "fieldname": "write_off_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Write Off Amount", "no_copy": 1, "options": "currency", @@ -1194,6 +1454,8 @@ { "fieldname": "base_write_off_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Write Off Amount (Company Currency)", "no_copy": 1, "options": "Company:company:default_currency", @@ -1205,16 +1467,22 @@ "depends_on": "is_pos", "fieldname": "write_off_outstanding_amount_automatically", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Write Off Outstanding Amount", "print_hide": 1 }, { "fieldname": "column_break_74", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "write_off_account", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Write Off Account", "options": "Account", "print_hide": 1 @@ -1222,6 +1490,8 @@ { "fieldname": "write_off_cost_center", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Write Off Cost Center", "options": "Cost Center", "print_hide": 1 @@ -1231,12 +1501,16 @@ "collapsible_depends_on": "terms", "fieldname": "terms_section_break", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Terms and Conditions", "oldfieldtype": "Section Break" }, { "fieldname": "tc_name", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Terms", "oldfieldname": "tc_name", "oldfieldtype": "Link", @@ -1246,6 +1520,8 @@ { "fieldname": "terms", "fieldtype": "Text Editor", + "hide_days": 1, + "hide_seconds": 1, "label": "Terms and Conditions Details", "oldfieldname": "terms", "oldfieldtype": "Text Editor" @@ -1254,12 +1530,16 @@ "collapsible": 1, "fieldname": "edit_printing_settings", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Printing Settings" }, { "allow_on_submit": 1, "fieldname": "letter_head", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Letter Head", "oldfieldname": "letter_head", "oldfieldtype": "Select", @@ -1271,24 +1551,32 @@ "default": "0", "fieldname": "group_same_items", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Group same items", "print_hide": 1 }, { "fieldname": "language", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Print Language", "print_hide": 1, "read_only": 1 }, { "fieldname": "column_break_84", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "select_print_heading", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Print Heading", "no_copy": 1, "oldfieldname": "select_print_heading", @@ -1302,11 +1590,15 @@ "depends_on": "customer", "fieldname": "more_information", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "More Information" }, { "fieldname": "inter_company_invoice_reference", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Inter Company Invoice Reference", "options": "Purchase Invoice", "read_only": 1 @@ -1315,6 +1607,8 @@ "fieldname": "customer_group", "fieldtype": "Link", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Customer Group", "options": "Customer Group", "print_hide": 1 @@ -1322,6 +1616,8 @@ { "fieldname": "campaign", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Campaign", "oldfieldname": "campaign", "oldfieldtype": "Link", @@ -1332,6 +1628,8 @@ "default": "0", "fieldname": "is_discounted", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Is Discounted", "no_copy": 1, "read_only": 1 @@ -1339,12 +1637,16 @@ { "fieldname": "col_break23", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "width": "50%" }, { "default": "Draft", "fieldname": "status", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Status", "no_copy": 1, @@ -1355,6 +1657,8 @@ { "fieldname": "source", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Source", "oldfieldname": "source", "oldfieldtype": "Select", @@ -1365,6 +1669,8 @@ "collapsible": 1, "fieldname": "more_info", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Accounting Details", "oldfieldtype": "Section Break", "options": "fa fa-file-text", @@ -1373,6 +1679,8 @@ { "fieldname": "debit_to", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Debit To", "oldfieldname": "debit_to", "oldfieldtype": "Link", @@ -1385,6 +1693,8 @@ "fieldname": "party_account_currency", "fieldtype": "Link", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Party Account Currency", "no_copy": 1, "options": "Currency", @@ -1395,6 +1705,8 @@ "default": "No", "fieldname": "is_opening", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Is Opening Entry", "oldfieldname": "is_opening", "oldfieldtype": "Select", @@ -1404,6 +1716,8 @@ { "fieldname": "c_form_applicable", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "C-Form Applicable", "no_copy": 1, "options": "No\nYes", @@ -1412,6 +1726,8 @@ { "fieldname": "c_form_no", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "C-Form No", "no_copy": 1, "options": "C-Form", @@ -1421,12 +1737,16 @@ { "fieldname": "column_break8", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Column Break", "print_hide": 1 }, { "fieldname": "remarks", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Remarks", "no_copy": 1, "oldfieldname": "remarks", @@ -1438,6 +1758,8 @@ "collapsible_depends_on": "sales_partner", "fieldname": "sales_team_section_break", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Commission", "oldfieldtype": "Section Break", "options": "fa fa-group", @@ -1446,6 +1768,8 @@ { "fieldname": "sales_partner", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Partner", "oldfieldname": "sales_partner", "oldfieldtype": "Link", @@ -1455,6 +1779,8 @@ { "fieldname": "column_break10", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Column Break", "print_hide": 1, "width": "50%" @@ -1462,6 +1788,8 @@ { "fieldname": "commission_rate", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Commission Rate (%)", "oldfieldname": "commission_rate", "oldfieldtype": "Currency", @@ -1470,6 +1798,8 @@ { "fieldname": "total_commission", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Commission", "oldfieldname": "total_commission", "oldfieldtype": "Currency", @@ -1481,6 +1811,8 @@ "collapsible_depends_on": "sales_team", "fieldname": "section_break2", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Team", "print_hide": 1 }, @@ -1488,6 +1820,8 @@ "allow_on_submit": 1, "fieldname": "sales_team", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Team1", "oldfieldname": "sales_team", "oldfieldtype": "Table", @@ -1495,14 +1829,19 @@ "print_hide": 1 }, { + "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Subscription Section" }, { "allow_on_submit": 1, "fieldname": "from_date", "fieldtype": "Date", + "hide_days": 1, + "hide_seconds": 1, "label": "From Date", "no_copy": 1, "print_hide": 1 @@ -1511,18 +1850,24 @@ "allow_on_submit": 1, "fieldname": "to_date", "fieldtype": "Date", + "hide_days": 1, + "hide_seconds": 1, "label": "To Date", "no_copy": 1, "print_hide": 1 }, { "fieldname": "column_break_140", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "auto_repeat", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Auto Repeat", "no_copy": 1, "options": "Auto Repeat", @@ -1534,12 +1879,16 @@ "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", + "hide_days": 1, + "hide_seconds": 1, "label": "Update Auto Repeat Reference" }, { "fieldname": "against_income_account", "fieldtype": "Small Text", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Against Income Account", "no_copy": 1, "oldfieldname": "against_income_account", @@ -1551,6 +1900,8 @@ "fieldname": "pos_total_qty", "fieldtype": "Float", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Total Qty", "print_hide": 1, "print_hide_if_no_value": 1, @@ -1560,17 +1911,23 @@ "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Accounting Dimensions" }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "fetch_from": "customer.is_internal_customer", "fieldname": "is_internal_customer", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Is Internal Customer", "read_only": 1 } @@ -1579,7 +1936,7 @@ "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-04-29 13:37:09.355300", + "modified": "2020-06-30 12:00:03.890180", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 05b85dabd4..bab5208370 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -582,14 +582,14 @@ class SalesInvoice(SellingController): def validate_item_code(self): for d in self.get('items'): - if not d.item_code: + if not d.item_code and self.is_opening == "No": msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True) def validate_warehouse(self): super(SalesInvoice, self).validate_warehouse() for d in self.get_item_list(): - if not d.warehouse 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): @@ -790,7 +790,8 @@ class SalesInvoice(SellingController): 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 + "cost_center": self.cost_center, + "project": self.project }, self.party_account_currency, item=self) ) @@ -845,7 +846,8 @@ class SalesInvoice(SellingController): "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount")) if account_currency==self.company_currency else flt(item.net_amount, item.precision("net_amount"))), - "cost_center": item.cost_center + "cost_center": item.cost_center, + "project": item.project or self.project }, account_currency, item=item) ) @@ -926,7 +928,8 @@ class SalesInvoice(SellingController): 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 + "cost_center": self.cost_center, + "project": self.project }, self.party_account_currency, item=self) ) @@ -959,7 +962,8 @@ class SalesInvoice(SellingController): 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 + "cost_center": self.cost_center, + "project": self.project }, self.party_account_currency, item=self) ) gl_entries.append( @@ -1109,7 +1113,10 @@ class SalesInvoice(SellingController): 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)): - points_earned = cint(eligible_amount/lp_details.collection_factor) + + collection_factor = lp_details.collection_factor if lp_details.collection_factor else 1.0 + points_earned = cint(eligible_amount/collection_factor) + doc = frappe.get_doc({ "doctype": "Loyalty Point Entry", "company": self.company, @@ -1450,11 +1457,17 @@ def get_inter_company_details(doc, doctype): 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))) + 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}) 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))) + party = get_internal_party(parties, "Customer", doc) return { @@ -1519,14 +1532,22 @@ 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') target_doc.company = details.get("company") target_doc.supplier = details.get("party") target_doc.buying_price_list = source_doc.selling_price_list + + if currency: + target_doc.currency = currency else: + 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 + if currency: + target_doc.currency = currency + doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json index ebe6e3da8d..11ebe6a573 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_records.json +++ b/erpnext/accounts/doctype/sales_invoice/test_records.json @@ -3,6 +3,7 @@ "company": "_Test Company", "conversion_rate": 1.0, "currency": "INR", + "cost_center": "_Test Cost Center - _TC", "customer": "_Test Customer", "customer_name": "_Test Customer", "debit_to": "_Test Receivable - _TC", @@ -37,7 +38,8 @@ "charge_type": "On Net Total", "description": "VAT", "doctype": "Sales Taxes and Charges", - "parentfield": "taxes", + "parentfield": "taxes", + "cost_center": "_Test Cost Center - _TC", "rate": 6 }, { @@ -45,7 +47,8 @@ "charge_type": "On Net Total", "description": "Service Tax", "doctype": "Sales Taxes and Charges", - "parentfield": "taxes", + "parentfield": "taxes", + "cost_center": "_Test Cost Center - _TC", "rate": 6.36 } ], @@ -76,6 +79,7 @@ "customer_name": "_Test Customer", "debit_to": "_Test Receivable - _TC", "doctype": "Sales Invoice", + "cost_center": "_Test Cost Center - _TC", "items": [ { "amount": 500.0, @@ -107,7 +111,8 @@ "charge_type": "On Net Total", "description": "VAT", "doctype": "Sales Taxes and Charges", - "parentfield": "taxes", + "parentfield": "taxes", + "cost_center": "_Test Cost Center - _TC", "rate": 16 }, { @@ -115,7 +120,8 @@ "charge_type": "On Net Total", "description": "Service Tax", "doctype": "Sales Taxes and Charges", - "parentfield": "taxes", + "parentfield": "taxes", + "cost_center": "_Test Cost Center - _TC", "rate": 10 } ], @@ -132,6 +138,7 @@ "customer_name": "_Test Customer", "debit_to": "_Test Receivable - _TC", "doctype": "Sales Invoice", + "cost_center": "_Test Cost Center - _TC", "items": [ { "cost_center": "_Test Cost Center - _TC", @@ -259,6 +266,7 @@ "customer_name": "_Test Customer", "debit_to": "_Test Receivable - _TC", "doctype": "Sales Invoice", + "cost_center": "_Test Cost Center - _TC", "items": [ { "cost_center": "_Test Cost Center - _TC", diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index c82a249843..ff4d6136e9 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1640,11 +1640,8 @@ class TestSalesInvoice(unittest.TestCase): si_doc = frappe.get_doc('Sales Invoice', si.name) self.assertEqual(si_doc.outstanding_amount, 0) - def test_sales_invoice_for_enable_allow_cost_center_in_entry_of_bs_account(self): + def test_sales_invoice_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 1 - accounts_settings.save() cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") @@ -1669,14 +1666,47 @@ class TestSalesInvoice(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) + + def test_sales_invoice_with_project_link(self): + from erpnext.projects.doctype.project.test_project import make_project - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() + 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' + }) - def test_sales_invoice_for_disable_allow_cost_center_in_entry_of_bs_account(self): - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 1 - accounts_settings.save() + sales_invoice = create_sales_invoice(do_not_save=1) + sales_invoice.items[0].project = item_project.project_name + sales_invoice.project = project.project_name + + sales_invoice.submit() + + expected_values = { + "Debtors - _TC": { + "project": project.project_name + }, + "Sales - _TC": { + "project": item_project.project_name + } + } + + 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) + + self.assertTrue(gl_entries) + + for gle in gl_entries: + self.assertEqual(expected_values[gle.account]["project"], gle.project) + + def test_sales_invoice_without_cost_center(self): cost_center = "_Test Cost Center - _TC" si = create_sales_invoice(debit_to="Debtors - _TC") @@ -1699,9 +1729,6 @@ class TestSalesInvoice(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - def test_deferred_revenue(self): deferred_account = create_account(account_name="Deferred Revenue", parent_account="Current Liabilities - _TC", company="_Test Company") @@ -1720,8 +1747,6 @@ class TestSalesInvoice(unittest.TestCase): si.save() si.submit() - from erpnext.accounts.deferred_revenue import convert_deferred_revenue_to_income - pda1 = frappe.get_doc(dict( doctype='Process Deferred Accounting', posting_date=nowdate(), @@ -1745,51 +1770,53 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, "2019-01-30") - def test_deferred_error_email(self): + 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") item.enable_deferred_revenue = 1 item.deferred_revenue_account = deferred_account item.no_of_months = 12 item.save() - si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_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-10" - si.items[0].service_end_date = "2019-03-15" + si.items[0].service_start_date = "2019-01-16" + si.items[0].service_end_date = "2019-03-31" si.items[0].deferred_revenue_account = deferred_account si.save() si.submit() - from erpnext.accounts.deferred_revenue import convert_deferred_revenue_to_income - - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - acc_settings.acc_frozen_upto = '2019-01-31' - acc_settings.save() - - pda = frappe.get_doc(dict( + pda1 = frappe.get_doc(dict( doctype='Process Deferred Accounting', - posting_date=nowdate(), + posting_date='2019-03-31', start_date="2019-01-01", end_date="2019-03-31", type="Income", company="_Test Company" )) - pda.insert() - pda.submit() + pda1.insert() + pda1.submit() - email = frappe.db.sql(""" select name from `tabEmail Queue` - where message like %(txt)s """, { - 'txt': "%%%s%%" % "Error while processing deferred accounting for {0}".format(pda.name) - }) + expected_gle = [ + [deferred_account, 10000.0, 0.0, "2019-01-31"], + ["Sales - _TC", 0.0, 10000.0, "2019-01-31"], + [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"] + ] - self.assertTrue(email) + check_gl_entries(self, si.name, expected_gle, "2019-01-30") - acc_settings.load_from_db() - acc_settings.acc_frozen_upto = None + 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): diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index b2294e4318..9bc24664d1 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -94,6 +94,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "section_break_54", "page_break" ], @@ -783,12 +784,18 @@ "fieldtype": "Link", "label": "Finance Book", "options": "Finance Book" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2019-12-04 12:22:38.517710", + "modified": "2020-03-11 12:24:41.749986", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index b2638c7827..d32a348741 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -45,7 +45,9 @@ class ShippingRule(Document): shipping_amount = 0.0 by_value = False - self.validate_countries(doc) + if doc.get_shipping_address(): + # validate country only if there is address + self.validate_countries(doc) if self.calculate_based_on == 'Net Total': value = doc.base_net_total 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 4d43919f62..83d7967f79 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -180,7 +180,7 @@ def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=No if company: condition += "and company =%s" % (company) if from_date and to_date: - condition += "and posting_date between %s and %s" % (company, from_date, to_date) + condition += "and posting_date between %s and %s" % (from_date, to_date) ## Appending the same supplier again if length of suppliers list is 1 ## since tuple of single element list contains None, For example ('Test Supplier 1', ) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index bfe35ab006..a245d63f52 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -140,10 +140,8 @@ def make_entry(args, adv_adj, update_outstanding): gle = frappe.new_doc("GL Entry") gle.update(args) gle.flags.ignore_permissions = 1 - gle.validate() - gle.db_insert() + gle.insert() gle.run_method("on_update_with_args", adv_adj, update_outstanding) - gle.flags.ignore_validate = True gle.submit() # check against budget diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json new file mode 100644 index 0000000000..ba1a779b4c --- /dev/null +++ b/erpnext/accounts/module_onboarding/accounts/accounts.json @@ -0,0 +1,50 @@ +{ + "allow_roles": [ + { + "role": "Accounts Manager" + }, + { + "role": "Accounts User" + } + ], + "creation": "2020-05-13 19:03:32.564049", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-08 14:06:09.033880", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Accounts", + "owner": "Administrator", + "steps": [ + { + "step": "Chart Of Accounts" + }, + { + "step": "Setup Taxes" + }, + { + "step": "Create a Product" + }, + { + "step": "Create a Supplier" + }, + { + "step": "Create Your First Purchase Invoice" + }, + { + "step": "Create a Customer" + }, + { + "step": "Create Your First Sales Invoice" + }, + { + "step": "Configure Account Settings" + } + ], + "subtitle": "Accounts, Invoices, Taxation, and more.", + "success_message": "The Accounts Module is all set up!", + "title": "Let's Set Up Your Accounts and Taxes." +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json new file mode 100644 index 0000000000..cbd022bfdb --- /dev/null +++ b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json @@ -0,0 +1,20 @@ +{ + "action": "Go to Page", + "creation": "2020-05-13 19:58:20.928127", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 17:40:28.410447", + "modified_by": "Administrator", + "name": "Chart Of Accounts", + "owner": "Administrator", + "path": "Tree/Account", + "reference_document": "Account", + "show_full_form": 0, + "title": "Review Chart Of Accounts", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/configure_account_settings/configure_account_settings.json b/erpnext/accounts/onboarding_step/configure_account_settings/configure_account_settings.json new file mode 100644 index 0000000000..c8be357de0 --- /dev/null +++ b/erpnext/accounts/onboarding_step/configure_account_settings/configure_account_settings.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:53:00.876946", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 1, + "is_skipped": 0, + "modified": "2020-05-14 18:06:25.212923", + "modified_by": "Administrator", + "name": "Configure Account Settings", + "owner": "Administrator", + "reference_document": "Accounts Settings", + "show_full_form": 1, + "title": "Configure Account Settings", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/accounts/onboarding_step/create_a_customer/create_a_customer.json new file mode 100644 index 0000000000..5a403b06cf --- /dev/null +++ b/erpnext/accounts/onboarding_step/create_a_customer/create_a_customer.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:46:41.831517", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 13:16:19.731719", + "modified_by": "Administrator", + "name": "Create a Customer", + "owner": "Administrator", + "reference_document": "Customer", + "show_full_form": 0, + "title": "Create a Customer", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_a_product/create_a_product.json b/erpnext/accounts/onboarding_step/create_a_product/create_a_product.json new file mode 100644 index 0000000000..d2068e167b --- /dev/null +++ b/erpnext/accounts/onboarding_step/create_a_product/create_a_product.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 18:16:06.624554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-12 18:30:02.489949", + "modified_by": "Administrator", + "name": "Create a Product", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create a Product", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/accounts/onboarding_step/create_a_supplier/create_a_supplier.json new file mode 100644 index 0000000000..7a64224bd4 --- /dev/null +++ b/erpnext/accounts/onboarding_step/create_a_supplier/create_a_supplier.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 22:09:10.043554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 22:09:10.043554", + "modified_by": "Administrator", + "name": "Create a Supplier", + "owner": "Administrator", + "reference_document": "Supplier", + "show_full_form": 0, + "title": "Create a Supplier", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json b/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json new file mode 100644 index 0000000000..3a2b8d3925 --- /dev/null +++ b/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 22:10:07.049704", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 22:10:07.049704", + "modified_by": "Administrator", + "name": "Create Your First Purchase Invoice", + "owner": "Administrator", + "reference_document": "Purchase Invoice", + "show_full_form": 1, + "title": "Create Your First Purchase Invoice ", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json b/erpnext/accounts/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json new file mode 100644 index 0000000000..473de5079f --- /dev/null +++ b/erpnext/accounts/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:48:21.019019", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 17:48:21.019019", + "modified_by": "Administrator", + "name": "Create Your First Sales Invoice", + "owner": "Administrator", + "reference_document": "Sales Invoice", + "show_full_form": 1, + "title": "Create Your First Sales Invoice ", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json new file mode 100644 index 0000000000..8e0006762d --- /dev/null +++ b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-13 19:29:43.844463", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 17:40:16.014413", + "modified_by": "Administrator", + "name": "Setup Taxes", + "owner": "Administrator", + "reference_document": "Sales Taxes and Charges Template", + "show_full_form": 1, + "title": "Lets create a Tax Template for Sales ", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py index 69f9907a8d..7df090bf62 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py @@ -21,7 +21,7 @@ def reconcile(bank_transaction, payment_doctype, payment_name): if payment_doctype == "Payment Entry" and payment_entry.unallocated_amount > transaction.unallocated_amount: frappe.throw(_("The unallocated amount of Payment Entry {0} \ is greater than the Bank Transaction's unallocated amount").format(payment_name)) - + if transaction.unallocated_amount == 0: frappe.throw(_("This bank transaction is already fully reconciled")) @@ -289,6 +289,7 @@ def get_matching_transactions_payments(description_matching): else: return [] +@frappe.whitelist() def payment_entry_query(doctype, txt, searchfield, start, page_len, filters): account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account") if not account: @@ -317,6 +318,7 @@ def payment_entry_query(doctype, txt, searchfield, start, page_len, filters): } ) +@frappe.whitelist() def journal_entry_query(doctype, txt, searchfield, start, page_len, filters): account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account") @@ -352,6 +354,7 @@ def journal_entry_query(doctype, txt, searchfield, start, page_len, filters): } ) +@frappe.whitelist() def sales_invoices_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" SELECT diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 528fb4e113..b764eff12c 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -388,7 +388,7 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup from erpnext.accounts.doctype.tax_rule.tax_rule import get_tax_template, get_party_details args = { party_type.lower(): party, - "company": company + "company": company } if tax_category: @@ -602,10 +602,14 @@ def get_party_shipping_address(doctype, name): else: return '' -def get_partywise_advanced_payment_amount(party_type, posting_date = None, company=None): +def get_partywise_advanced_payment_amount(party_type, posting_date = None, future_payment=0, company=None): cond = "1=1" if posting_date: - cond = "posting_date <= '{0}'".format(posting_date) + if future_payment: + 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(company) diff --git a/erpnext/accounts/report/account_balance/test_account_balance.py b/erpnext/accounts/report/account_balance/test_account_balance.py index 5544fc4673..b6ced312d0 100644 --- a/erpnext/accounts/report/account_balance/test_account_balance.py +++ b/erpnext/accounts/report/account_balance/test_account_balance.py @@ -61,7 +61,7 @@ def make_sales_invoice(): debit_to = 'Debtors - _TC2', income_account = 'Sales - _TC2', expense_account = 'Cost of Goods Sold - _TC2', - cost_center = '_Test Company 2 - _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 4e09f99ae3..6abd6e5cf7 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -17,41 +17,6 @@ frappe.query_reports["Accounts Payable"] = { "fieldtype": "Date", "default": frappe.datetime.get_today() }, - { - "fieldname":"ageing_based_on", - "label": __("Ageing Based On"), - "fieldtype": "Select", - "options": 'Posting Date\nDue Date\nSupplier Invoice Date', - "default": "Due Date" - }, - { - "fieldname":"range1", - "label": __("Ageing Range 1"), - "fieldtype": "Int", - "default": "30", - "reqd": 1 - }, - { - "fieldname":"range2", - "label": __("Ageing Range 2"), - "fieldtype": "Int", - "default": "60", - "reqd": 1 - }, - { - "fieldname":"range3", - "label": __("Ageing Range 3"), - "fieldtype": "Int", - "default": "90", - "reqd": 1 - }, - { - "fieldname":"range4", - "label": __("Ageing Range 4"), - "fieldtype": "Int", - "default": "120", - "reqd": 1 - }, { "fieldname":"finance_book", "label": __("Finance Book"), @@ -88,6 +53,41 @@ frappe.query_reports["Accounts Payable"] = { } } }, + { + "fieldname":"ageing_based_on", + "label": __("Ageing Based On"), + "fieldtype": "Select", + "options": 'Posting Date\nDue Date\nSupplier Invoice Date', + "default": "Due Date" + }, + { + "fieldname":"range1", + "label": __("Ageing Range 1"), + "fieldtype": "Int", + "default": "30", + "reqd": 1 + }, + { + "fieldname":"range2", + "label": __("Ageing Range 2"), + "fieldtype": "Int", + "default": "60", + "reqd": 1 + }, + { + "fieldname":"range3", + "label": __("Ageing Range 3"), + "fieldtype": "Int", + "default": "90", + "reqd": 1 + }, + { + "fieldname":"range4", + "label": __("Ageing Range 4"), + "fieldtype": "Int", + "default": "120", + "reqd": 1 + }, { "fieldname":"payment_terms_template", "label": __("Payment Terms Template"), @@ -135,12 +135,5 @@ frappe.query_reports["Accounts Payable"] = { } } -erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Accounts Payable"].filters.splice(9, 0 ,{ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] - }); -}); +erpnext.utils.add_dimensions('Accounts Payable', 9); diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index d5f18b0982..9c6b0639c0 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -104,12 +104,5 @@ frappe.query_reports["Accounts Payable Summary"] = { } } -erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Accounts Payable Summary"].filters.splice(9, 0 ,{ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] - }); -}); +erpnext.utils.add_dimensions('Accounts Payable Summary', 9); diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 6208eb6946..c999eb9b8e 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -17,41 +17,6 @@ frappe.query_reports["Accounts Receivable"] = { "fieldtype": "Date", "default": frappe.datetime.get_today() }, - { - "fieldname":"ageing_based_on", - "label": __("Ageing Based On"), - "fieldtype": "Select", - "options": 'Posting Date\nDue Date', - "default": "Due Date" - }, - { - "fieldname":"range1", - "label": __("Ageing Range 1"), - "fieldtype": "Int", - "default": "30", - "reqd": 1 - }, - { - "fieldname":"range2", - "label": __("Ageing Range 2"), - "fieldtype": "Int", - "default": "60", - "reqd": 1 - }, - { - "fieldname":"range3", - "label": __("Ageing Range 3"), - "fieldtype": "Int", - "default": "90", - "reqd": 1 - }, - { - "fieldname":"range4", - "label": __("Ageing Range 4"), - "fieldtype": "Int", - "default": "120", - "reqd": 1 - }, { "fieldname":"finance_book", "label": __("Finance Book"), @@ -101,6 +66,41 @@ frappe.query_reports["Accounts Receivable"] = { } } }, + { + "fieldname":"ageing_based_on", + "label": __("Ageing Based On"), + "fieldtype": "Select", + "options": 'Posting Date\nDue Date', + "default": "Due Date" + }, + { + "fieldname":"range1", + "label": __("Ageing Range 1"), + "fieldtype": "Int", + "default": "30", + "reqd": 1 + }, + { + "fieldname":"range2", + "label": __("Ageing Range 2"), + "fieldtype": "Int", + "default": "60", + "reqd": 1 + }, + { + "fieldname":"range3", + "label": __("Ageing Range 3"), + "fieldtype": "Int", + "default": "90", + "reqd": 1 + }, + { + "fieldname":"range4", + "label": __("Ageing Range 4"), + "fieldtype": "Int", + "default": "120", + "reqd": 1 + }, { "fieldname":"customer_group", "label": __("Customer Group"), @@ -113,12 +113,6 @@ frappe.query_reports["Accounts Receivable"] = { "fieldtype": "Link", "options": "Payment Terms Template" }, - { - "fieldname":"territory", - "label": __("Territory"), - "fieldtype": "Link", - "options": "Territory" - }, { "fieldname":"sales_partner", "label": __("Sales Partner"), @@ -131,6 +125,12 @@ frappe.query_reports["Accounts Receivable"] = { "fieldtype": "Link", "options": "Sales Person" }, + { + "fieldname":"territory", + "label": __("Territory"), + "fieldtype": "Link", + "options": "Territory" + }, { "fieldname": "group_by_party", "label": __("Group By Customer"), @@ -199,12 +199,5 @@ frappe.query_reports["Accounts Receivable"] = { } } -erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Accounts Receivable"].filters.splice(9, 0 ,{ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] - }); -}); +erpnext.utils.add_dimensions('Accounts Receivable', 9); diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e9c286fcf0..66aa18058b 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -169,9 +169,11 @@ class ReceivablePayableReport(object): def append_subtotal_row(self, party): sub_total_row = self.total_row_map.get(party) - self.data.append(sub_total_row) - self.data.append({}) - self.update_sub_total_row(sub_total_row, 'Total') + + if sub_total_row: + self.data.append(sub_total_row) + self.data.append({}) + self.update_sub_total_row(sub_total_row, 'Total') def get_voucher_balance(self, gle): if self.filters.get("sales_person"): @@ -232,7 +234,8 @@ class ReceivablePayableReport(object): if self.filters.get('group_by_party'): self.append_subtotal_row(self.previous_party) - self.data.append(self.total_row_map.get('Total')) + if self.data: + self.data.append(self.total_row_map.get('Total')) def append_row(self, row): self.allocate_future_payments(row) @@ -534,7 +537,7 @@ class ReceivablePayableReport(object): def get_ageing_data(self, entry_date, row): # [0-30, 30-60, 60-90, 90-120, 120-above] - row.range1 = row.range2 = row.range3 = row.range4 = range5 = 0.0 + row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 if not (self.age_as_on and entry_date): return @@ -546,7 +549,7 @@ class ReceivablePayableReport(object): 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]): - if row.age <= days: + if cint(row.age) <= cint(days): index = i break @@ -559,6 +562,14 @@ class ReceivablePayableReport(object): conditions, values = self.prepare_conditions() order_by = self.get_order_by_condition() + if self.filters.show_future_payments: + values.insert(2, self.filters.report_date) + + date_condition = """AND (posting_date <= %s + OR (against_voucher IS NULL AND DATE(creation) <= %s))""" + else: + date_condition = "AND posting_date <=%s" + if self.filters.get(scrub(self.party_type)): select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit" else: @@ -574,9 +585,8 @@ class ReceivablePayableReport(object): docstatus < 2 and party_type=%s and (party is not null and party != '') - and posting_date <= %s - {1} {2}""" - .format(select_fields, conditions, order_by), values, as_dict=True) + {1} {2} {3}""" + .format(select_fields, date_condition, conditions, order_by), values, as_dict=True) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index f0274b4472..2ff5b531c5 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -63,7 +63,7 @@ def make_sales_invoice(): debit_to = 'Debtors - _TC2', income_account = 'Sales - _TC2', expense_account = 'Cost of Goods Sold - _TC2', - cost_center = '_Test Company 2 - _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)) @@ -83,14 +83,14 @@ 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 = '_Test Company 2 - _TC2', - is_return = 1, - return_against = docname) + 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.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index b316f108d0..305cddb102 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -111,7 +111,12 @@ frappe.query_reports["Accounts Receivable Summary"] = { "fieldname":"based_on_payment_terms", "label": __("Based On Payment Terms"), "fieldtype": "Check", - } + }, + { + "fieldname":"show_future_payments", + "label": __("Show Future Payments"), + "fieldtype": "Check", + }, ], onload: function(report) { @@ -122,11 +127,4 @@ frappe.query_reports["Accounts Receivable Summary"] = { } } -erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Accounts Receivable Summary"].filters.splice(9, 0 ,{ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] - }); -}); +erpnext.utils.add_dimensions('Accounts Receivable Summary', 9); 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 aa6b42e89d..657b3e8f20 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -33,7 +33,7 @@ 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.company) or {} + self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {} for party, party_dict in iteritems(self.party_total): if party_dict.outstanding == 0: 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 d7efbad240..5001ad9f12 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 @@ -93,7 +93,7 @@ def get_assets(filters): sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period from (SELECT a.asset_category, - ifnull(sum(case when ds.schedule_date < %(from_date)s then + ifnull(sum(case when ds.schedule_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then ds.depreciation_amount else 0 @@ -111,13 +111,11 @@ def get_assets(filters): 0 end), 0) as depreciation_amount_during_the_period from `tabAsset` a, `tabDepreciation Schedule` ds - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != '' group by a.asset_category union SELECT a.asset_category, - ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 - and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) - then + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then 0 else a.opening_accumulated_depreciation diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js index c4c24c0bab..4a4ad4d71c 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.js +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js @@ -4,6 +4,8 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statements); + erpnext.utils.add_dimensions('Balance Sheet', 10); + frappe.query_reports["Balance Sheet"]["filters"].push({ "fieldname": "accumulated_values", "label": __("Accumulated Values"), 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 49c1d0f2cc..9c9ada871c 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -29,41 +29,76 @@ def execute(filters=None): for dimension in dimensions: dimension_items = cam_map.get(dimension) if dimension_items: - for account, monthwise_data in iteritems(dimension_items): - row = [dimension, account] - totals = [0, 0, 0] - for year in get_fiscal_years(filters): - last_total = 0 - for relevant_months in period_month_ranges: - period_data = [0, 0, 0] - for month in relevant_months: - if monthwise_data.get(year[0]): - month_data = monthwise_data.get(year[0]).get(month, {}) - for i, fieldname in enumerate(["target", "actual", "variance"]): - value = flt(month_data.get(fieldname)) - period_data[i] += value - totals[i] += value + 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 + FROM `tabDistributed Cost Center` + WHERE cost_center IN %(dimension)s + AND parent NOT IN %(dimension)s + GROUP BY parent''',{'dimension':[dimension]}) + if DCC_allocation: + filters['budget_against_filter'] = [DCC_allocation[0][0]] + cam_map = get_dimension_account_month_map(filters) + dimension_items = 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]) - period_data[0] += last_total + chart = get_chart_data(filters, columns, data) - if filters.get("show_cumulative"): - last_total = period_data[0] - period_data[1] + return columns, data, None, chart - period_data[2] = period_data[0] - period_data[1] - row += period_data - totals[2] = totals[0] - totals[1] - if filters["period"] != "Yearly": - row += totals - data.append(row) +def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation): - return columns, data + for account, monthwise_data in iteritems(dimension_items): + row = [dimension, account] + totals = [0, 0, 0] + for year in get_fiscal_years(filters): + last_total = 0 + for relevant_months in period_month_ranges: + period_data = [0, 0, 0] + for month in relevant_months: + if monthwise_data.get(year[0]): + month_data = monthwise_data.get(year[0]).get(month, {}) + for i, fieldname in enumerate(["target", "actual", "variance"]): + value = flt(month_data.get(fieldname)) + period_data[i] += value + totals[i] += value + + 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) + + 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" : + row += totals + data.append(row) + + return data def get_columns(filters): columns = [ - _(filters.get("budget_against")) - + ":Link/%s:150" % (filters.get("budget_against")), - _("Account") + ":Link/Account: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 + } ] group_months = False if filters["period"] == "Monthly" else True @@ -79,7 +114,12 @@ def get_columns(filters): _("Variance ") + " " + str(year[0]) ] for label in labels: - columns.append(label + ":Float:150") + columns.append({ + 'label': label, + 'fieldtype': 'Float', + 'fieldname': frappe.scrub(label), + 'width': 150 + }) else: for label in [ _("Budget") + " (%s)" + " " + str(year[0]), @@ -95,14 +135,23 @@ def get_columns(filters): else: label = label % formatdate(from_date, format_string="MMM") - columns.append(label + ":Float:150") + columns.append({ + 'label': label, + 'fieldtype': 'Float', + 'fieldname': frappe.scrub(label), + 'width': 150 + }) if filters["period"] != "Yearly": - return columns + [ - _("Total Budget") + ":Float:150", - _("Total Actual") + ":Float:150", - _("Total Variance") + ":Float:150" - ] + for label in [_("Total Budget"), _("Total Actual"), _("Total Variance")]: + columns.append({ + 'label': label, + 'fieldtype': 'Float', + 'fieldname': frappe.scrub(label), + 'width': 150 + }) + + return columns else: return columns @@ -173,7 +222,7 @@ def get_dimension_target_details(filters): filters.budget_against, filters.company, ] - + filters.get("budget_against_filter") + + (filters.get("budget_against_filter") or []) ), as_dict=True) @@ -305,3 +354,49 @@ def get_fiscal_years(filters): }) return fiscal_year + +def get_chart_data(filters, columns, data): + + if not data: + return None + + labels = [] + + fiscal_year = get_fiscal_years(filters) + group_months = False if filters["period"] == "Monthly" else True + + for year in fiscal_year: + for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): + 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") + labels.append(label) + else: + label = formatdate(from_date, format_string="MMM") + labels.append(label) + + no_of_columns = len(labels) + + budget_values, actual_values = [0] * no_of_columns, [0] * no_of_columns + for d in data: + values = d[2:] + index = 0 + + for i in range(no_of_columns): + budget_values[i] += values[index] + 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} + ] + } + } + diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js index e5d0c89918..a984bf46b5 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -5,6 +5,8 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Cash Flow"] = $.extend({}, erpnext.financial_statements); + erpnext.utils.add_dimensions('Cash Flow', 10); + // The last item in the array is the definition for Presentation Currency // filter. It won't be used in cash flow for now so we pop it. Please take // of this if you are working here. diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py index 8566f5375a..fe2bc725e0 100644 --- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py +++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py @@ -334,10 +334,9 @@ 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.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, company=filters.company) mappers = get_mappers_from_db() @@ -396,7 +395,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate 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 + 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)) @@ -405,7 +404,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate 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 + 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)) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index 38fd5fa278..09479221fb 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -33,7 +33,6 @@ frappe.query_reports["Consolidated Financial Statement"] = { "fieldname":"period_start_date", "label": __("Start Date"), "fieldtype": "Date", - "default": frappe.datetime.nowdate(), "hidden": 1, "reqd": 1 }, @@ -41,7 +40,6 @@ frappe.query_reports["Consolidated Financial Statement"] = { "fieldname":"period_end_date", "label": __("End Date"), "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.nowdate(), 12), "hidden": 1, "reqd": 1 }, @@ -106,5 +104,16 @@ frappe.query_reports["Consolidated Financial Statement"] = { value = $value.wrap("

").parent().html(); } return value; + }, + onload: function() { + let fiscal_year = frappe.defaults.get_user_default("fiscal_year") + + frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); + frappe.query_report.set_filter_value({ + period_start_date: fy.year_start_date, + period_end_date: fy.year_end_date + }); + }); } } diff --git a/erpnext/accounts/report/financial_statements.html b/erpnext/accounts/report/financial_statements.html index 50947ecf5e..2bb09cf0dc 100644 --- a/erpnext/accounts/report/financial_statements.html +++ b/erpnext/accounts/report/financial_statements.html @@ -44,7 +44,7 @@ - {% for(let j=0, k=data.length-1; j { - frappe.query_reports["General Ledger"].filters.splice(15, 0 ,{ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] - }); -}); +erpnext.utils.add_dimensions('General Ledger', 15) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 6afe208b5b..fcd36e4e6e 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -128,18 +128,53 @@ def get_gl_entries(filters): filters['company_fb'] = frappe.db.get_value("Company", filters.get("company"), 'default_finance_book') + distributed_cost_center_query = "" + 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, + credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """ + + distributed_cost_center_query = """ + UNION ALL + SELECT name as gl_entry, + posting_date, + account, + party_type, + party, + voucher_type, + voucher_no, + cost_center, project, + against_voucher_type, + against_voucher, + account_currency, + remarks, against, + is_opening, `tabGL Entry`.creation {select_fields_with_percentage} + FROM `tabGL Entry`, + ( + 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 + ) as DCC_allocation + WHERE company=%(company)s + {conditions} + AND posting_date <= %(to_date)s + AND cost_center = DCC_allocation.parent + """.format(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( """ select name as gl_entry, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, project, against_voucher_type, against_voucher, account_currency, - remarks, against, is_opening {select_fields} + remarks, against, is_opening, creation {select_fields} from `tabGL Entry` where company=%(company)s {conditions} + {distributed_cost_center_query} {order_by_statement} """.format( - select_fields=select_fields, conditions=get_conditions(filters), + 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) @@ -296,7 +331,7 @@ def get_accountwise_gle(filters, gl_entries, gle_map): data[key].debit_in_account_currency += flt(gle.debit_in_account_currency) data[key].credit_in_account_currency += flt(gle.credit_in_account_currency) - if data[key].against_voucher: + if data[key].against_voucher and gle.against_voucher: data[key].against_voucher += ', ' + gle.against_voucher from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js index f88906a0f1..b709ab9b57 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.js @@ -4,11 +4,18 @@ frappe.query_reports["Item-wise Purchase Register"] = { "filters": [ { - "fieldname":"date_range", - "label": __("Date Range"), - "fieldtype": "DateRange", - "default": [frappe.datetime.add_months(frappe.datetime.get_today(),-1), frappe.datetime.get_today()], - "reqd": 1 + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "reqd": 1, + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1, }, { "fieldname": "item_code", 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 1f78c7a006..3445df7206 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 @@ -14,7 +14,6 @@ def execute(filters=None): def _execute(filters=None, additional_table_columns=None, additional_query_columns=None): if not filters: filters = {} - filters.update({"from_date": filters.get("date_range")[0], "to_date": filters.get("date_range")[1]}) columns = get_columns(additional_table_columns, filters) company_currency = erpnext.get_company_currency(filters.company) @@ -266,13 +265,6 @@ def get_columns(additional_table_columns, filters): 'fieldtype': 'Currency', 'options': 'currency', 'width': 100 - }, - { - 'fieldname': 'currency', - 'label': _('Currency'), - 'fieldtype': 'Currency', - 'width': 80, - 'hidden': 1 } ] diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js index 8a9c76f26f..39fb3ca5ee 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js @@ -4,11 +4,18 @@ frappe.query_reports["Item-wise Sales Register"] = { "filters": [ { - "fieldname": "date_range", - "label": __("Date Range"), - "fieldtype": "DateRange", - "default": [frappe.datetime.add_months(frappe.datetime.get_today(),-1), frappe.datetime.get_today()], - "reqd": 1 + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "reqd": 1, + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1, }, { "fieldname": "customer", 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 92a22e62f1..a05dcd75ce 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 @@ -14,7 +14,6 @@ def execute(filters=None): def _execute(filters=None, additional_table_columns=None, additional_query_columns=None): if not filters: filters = {} - filters.update({"from_date": filters.get("date_range") and filters.get("date_range")[0], "to_date": filters.get("date_range") and filters.get("date_range")[1]}) columns = get_columns(additional_table_columns, filters) company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency") @@ -224,7 +223,7 @@ def get_columns(additional_table_columns, filters): } ] - if filters.get('group_by') != 'Terriotory': + if filters.get('group_by') != 'Territory': columns.extend([ { 'label': _("Territory"), @@ -305,13 +304,6 @@ def get_columns(additional_table_columns, filters): 'fieldtype': 'Currency', 'options': 'currency', 'width': 100 - }, - { - 'fieldname': 'currency', - 'label': _('Currency'), - 'fieldtype': 'Currency', - 'width': 80, - 'hidden': 1 } ] @@ -537,6 +529,13 @@ def get_tax_accounts(item_list, columns, company_currency, 'fieldtype': 'Currency', 'options': 'currency', 'width': 100 + }, + { + 'fieldname': 'currency', + 'label': _('Currency'), + 'fieldtype': 'Currency', + 'width': 80, + 'hidden': 1 } ] diff --git a/erpnext/accounts/report/ordered_items_to_be_billed/ordered_items_to_be_billed.json b/erpnext/accounts/report/ordered_items_to_be_billed/ordered_items_to_be_billed.json deleted file mode 100644 index c983dc9629..0000000000 --- a/erpnext/accounts/report/ordered_items_to_be_billed/ordered_items_to_be_billed.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-02-21 14:26:44", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-11-06 13:04:51.559061", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Ordered Items To Be Billed", - "owner": "Administrator", - "query": "select \n `tabSales Order`.`name` as \"Sales Order:Link/Sales Order:120\",\n `tabSales Order`.`customer` as \"Customer:Link/Customer:120\",\n `tabSales Order`.`customer_name` as \"Customer Name:150\",\n`tabSales Order`.`status` as \"Status\",\n `tabSales Order`.`transaction_date` as \"Date:Date\",\n `tabSales Order`.`project` as \"Project\",\n `tabSales Order Item`.item_code as \"Item:Link/Item:120\",\n `tabSales Order Item`.base_amount as \"Amount:Currency:110\",\n (`tabSales Order Item`.billed_amt * ifnull(`tabSales Order`.conversion_rate, 1)) as \"Billed Amount:Currency:110\",\n (`tabSales Order Item`.base_amount - (`tabSales Order Item`.billed_amt * ifnull(`tabSales Order`.conversion_rate, 1))) as \"Pending Amount:Currency:120\",\n `tabSales Order Item`.item_name as \"Item Name::150\",\n `tabSales Order Item`.description as \"Description::200\",\n `tabSales Order`.`company` as \"Company:Link/Company:\"\nfrom\n `tabSales Order`, `tabSales Order Item`\nwhere\n `tabSales Order Item`.`parent` = `tabSales Order`.`name`\n and `tabSales Order`.docstatus = 1\n and `tabSales Order`.status != \"Closed\"\n and `tabSales Order Item`.amount > 0\n and `tabSales Order Item`.billed_amt < `tabSales Order Item`.amount\norder by `tabSales Order`.transaction_date asc", - "ref_doctype": "Sales Invoice", - "report_name": "Ordered Items To Be Billed", - "report_type": "Script Report", - "roles": [ - { - "role": "Accounts Manager" - }, - { - "role": "Accounts User" - } - ] -} \ No newline at end of file diff --git a/erpnext/accounts/report/ordered_items_to_be_billed/ordered_items_to_be_billed.py b/erpnext/accounts/report/ordered_items_to_be_billed/ordered_items_to_be_billed.py deleted file mode 100644 index ec0d2f39f3..0000000000 --- a/erpnext/accounts/report/ordered_items_to_be_billed/ordered_items_to_be_billed.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data - -def execute(filters=None): - columns = get_column() - args = get_args() - data = get_ordered_to_be_billed_data(args) - return columns, data - -def get_column(): - return [ - _("Sales Order") + ":Link/Sales Order:120", _("Status") + "::120", _("Date") + ":Date:100", - _("Suplier") + ":Link/Customer:120", _("Customer Name") + "::120", - _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120", - _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Pending Amount") + ":Currency:100", - _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120", - ] - -def get_args(): - return {'doctype': 'Sales Order', 'party': 'customer', - 'date': 'transaction_date', 'order': 'transaction_date', 'order_by': 'asc'} \ No newline at end of file diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js index 2b946c0540..1c461efbcd 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js @@ -6,6 +6,8 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financial_statements); + erpnext.utils.add_dimensions('Profit and Loss Statement', 10); + frappe.query_reports["Profit and Loss Statement"]["filters"].push( { "fieldname": "project", diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index 6e9b31f2f6..60e675f2f1 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -105,6 +105,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name): 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") for d in accounts: @@ -118,6 +119,19 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on): "currency": company_currency, "based_on": based_on } + 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 + 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]}) + if DCC_allocation: + for account in new_accounts: + if account['name'] == DCC_allocation[0][0]: + for value in value_fields: + d[value] += account[value]*(DCC_allocation[0][1]/100) for key in value_fields: row[key] = flt(d.get(key, 0.0), 3) diff --git a/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.js b/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.js deleted file mode 100644 index 24c9592c24..0000000000 --- a/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.query_reports["Purchase Order Items To Be Billed"] = { - "filters": [ - - ] -} diff --git a/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.json b/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.json deleted file mode 100644 index 3645ec02e1..0000000000 --- a/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-05-28 15:54:16", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-02-24 20:00:24.302988", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Purchase Order Items To Be Billed", - "owner": "Administrator", - "query": "select \n `tabPurchase Order`.`name` as \"Purchase Order:Link/Purchase Order:120\",\n `tabPurchase Order`.`transaction_date` as \"Date:Date:100\",\n\t`tabPurchase Order`.`supplier` as \"Supplier:Link/Supplier:120\",\n\t`tabPurchase Order`.`supplier_name` as \"Supplier Name::150\",\n\t`tabPurchase Order Item`.`project` as \"Project\",\n\t`tabPurchase Order Item`.item_code as \"Item Code:Link/Item:120\",\n\t`tabPurchase Order Item`.base_amount as \"Amount:Currency:100\",\n\t(`tabPurchase Order Item`.billed_amt * ifnull(`tabPurchase Order`.conversion_rate, 1)) as \"Billed Amount:Currency:100\", \n\t(`tabPurchase Order Item`.base_amount - (`tabPurchase Order Item`.billed_amt * ifnull(`tabPurchase Order`.conversion_rate, 1))) as \"Amount to Bill:Currency:100\",\n\t`tabPurchase Order Item`.item_name as \"Item Name::150\",\n\t`tabPurchase Order Item`.description as \"Description::200\",\n\t`tabPurchase Order`.company as \"Company:Link/Company:\"\nfrom\n\t`tabPurchase Order`, `tabPurchase Order Item`\nwhere\n\t`tabPurchase Order Item`.`parent` = `tabPurchase Order`.`name`\n\tand `tabPurchase Order`.docstatus = 1\n\tand `tabPurchase Order`.status != \"Closed\"\n and `tabPurchase Order Item`.amount > 0\n\tand (`tabPurchase Order Item`.billed_amt * ifnull(`tabPurchase Order`.conversion_rate, 1)) < `tabPurchase Order Item`.base_amount\norder by `tabPurchase Order`.transaction_date asc", - "ref_doctype": "Purchase Invoice", - "report_name": "Purchase Order Items To Be Billed", - "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Purchase User" - }, - { - "role": "Auditor" - }, - { - "role": "Accounts Manager" - } - ] -} \ No newline at end of file diff --git a/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.py b/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.py deleted file mode 100644 index 99d0a36813..0000000000 --- a/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data - -def execute(filters=None): - columns = get_column() - args = get_args() - data = get_ordered_to_be_billed_data(args) - return columns, data - -def get_column(): - return [ - _("Purchase Order") + ":Link/Purchase Order:120", _("Status") + "::120", _("Date") + ":Date:100", - _("Suplier") + ":Link/Supplier:120", _("Suplier Name") + "::120", - _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120", - _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Amount to Bill") + ":Currency:100", - _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120", - ] - -def get_args(): - return {'doctype': 'Purchase Order', 'party': 'supplier', - 'date': 'transaction_date', 'order': 'transaction_date', 'order_by': 'asc'} diff --git a/erpnext/accounts/report/purchase_register/purchase_register.js b/erpnext/accounts/report/purchase_register/purchase_register.js index b2b95b2b81..f34ea57163 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.js +++ b/erpnext/accounts/report/purchase_register/purchase_register.js @@ -56,11 +56,4 @@ frappe.query_reports["Purchase Register"] = { ] } -erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Purchase Register"].filters.splice(7, 0 ,{ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] - }); -}); \ No newline at end of file +erpnext.utils.add_dimensions('Purchase Register', 7); \ No newline at end of file diff --git a/erpnext/accounts/report/sales_register/sales_register.js b/erpnext/accounts/report/sales_register/sales_register.js index 9dee656d4a..85bbceab82 100644 --- a/erpnext/accounts/report/sales_register/sales_register.js +++ b/erpnext/accounts/report/sales_register/sales_register.js @@ -68,12 +68,5 @@ frappe.query_reports["Sales Register"] = { ] } -erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Sales Register"].filters.splice(7, 0 ,{ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] - }); -}); +erpnext.utils.add_dimensions('Sales Register', 7); 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 4ac0f65611..a9fb237a04 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -111,7 +111,7 @@ def get_gle_map(filters): # {"purchase_invoice": list of dict of all gle created for this invoice} gle_map = {} gle = frappe.db.get_all('GL Entry',\ - {"voucher_no": ["in", [d.get("name") for d in filters["invoices"]]]}, + {"voucher_no": ["in", [d.get("name") for d in filters["invoices"]]], 'is_cancelled': 0}, ["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date"]) for d in gle: diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index 07752e1e62..9c0854c5d3 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -102,14 +102,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "initial_depth": 3 } - erpnext.dimension_filters.forEach((dimension) => { - frappe.query_reports["Trial Balance"].filters.splice(6, 0 ,{ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] - }); - }); + erpnext.utils.add_dimensions('Trial Balance', 6); }); diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 8bd4399e60..5a699b6580 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -71,7 +71,8 @@ def get_data(filters): opening_balances = get_opening_balances(filters) #add filter inside list so that the query in financial_statements.py doesn't break - filters.project = [filters.project] + 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)) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5165495786..013c30d6ff 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -57,6 +57,9 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb frappe.cache().hset("fiscal_years", company, fiscal_years) + if not transaction_date and not fiscal_year: + return fiscal_years + if transaction_date: transaction_date = getdate(transaction_date) @@ -79,6 +82,23 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb 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 + } + 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")] + }) + 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: @@ -113,7 +133,7 @@ def get_balance_on(account=None, date=None, party_type=None, party=None, company acc = frappe.get_doc("Account", account) try: - year_start_date = get_fiscal_year(date, verbose=0)[1] + year_start_date = get_fiscal_year(date, company=company, verbose=0)[1] except FiscalYearError: if getdate(date) > getdate(nowdate()): # if fiscal year not found and the date is greater than today @@ -124,14 +144,12 @@ def get_balance_on(account=None, date=None, party_type=None, party=None, company # hence, assuming balance as 0.0 return 0.0 - allow_cost_center_in_entry_of_bs_account = get_allow_cost_center_in_entry_of_bs_account() - if account: report_type = acc.report_type else: report_type = "" - if cost_center and (allow_cost_center_in_entry_of_bs_account or 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 ( @@ -767,10 +785,10 @@ def get_children(doctype, parent, company, is_root=False): company_currency = frappe.get_cached_value('Company', company, "default_currency") for each in acc: each["company_currency"] = company_currency - each["balance"] = flt(get_balance_on(each.get("value"), in_account_currency=False)) + each["balance"] = flt(get_balance_on(each.get("value"), in_account_currency=False, company=company)) if each.account_currency != company_currency: - each["balance_in_account_currency"] = flt(get_balance_on(each.get("value"))) + each["balance_in_account_currency"] = flt(get_balance_on(each.get("value"), company=company)) return acc @@ -817,7 +835,7 @@ def create_payment_gateway_account(gateway): pass @frappe.whitelist() -def update_cost_center(docname, cost_center_name, cost_center_number, company): +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. @@ -833,7 +851,7 @@ def update_cost_center(docname, cost_center_name, cost_center_number, company): new_name = get_autoname_with_number(cost_center_number, cost_center_name, docname, company) if docname != new_name: - frappe.rename_doc("Cost Center", docname, new_name, force=1) + 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): @@ -877,11 +895,6 @@ def get_coa(doctype, parent, is_root, chart=None): return accounts -def get_allow_cost_center_in_entry_of_bs_account(): - def generator(): - return cint(frappe.db.get_value('Accounts Settings', None, 'allow_cost_center_in_entry_of_bs_account')) - return frappe.local_cache("get_allow_cost_center_in_entry_of_bs_account", (), generator, regenerate_if_none=True) - def get_stock_accounts(company): return frappe.get_all("Account", filters = { "account_type": "Stock", @@ -942,4 +955,4 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1): gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) - return gl_entries \ No newline at end of file + return gl_entries diff --git a/erpnext/assets/dashboard_fixtures.py b/erpnext/assets/dashboard_fixtures.py new file mode 100644 index 0000000000..7f3c1de406 --- /dev/null +++ b/erpnext/assets/dashboard_fixtures.py @@ -0,0 +1,179 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import json +from frappe.utils import nowdate, add_months, get_date_str +from frappe import _ +from erpnext.accounts.dashboard_fixtures import _get_fiscal_year +from erpnext.buying.dashboard_fixtures import get_company_for_dashboards + +def get_data(): + + fiscal_year = _get_fiscal_year(nowdate()) + + 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')) + + 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"} + ] + }] + +def get_charts(fiscal_year, year_start_date, year_end_date): + company = get_company_for_dashboards() + return [ + { + "name": "Asset Value Analytics", + "chart_name": _("Asset Value Analytics"), + "chart_type": "Report", + "report_name": "Fixed Asset Register", + "is_custom": 1, + "group_by_type": "Count", + "number_of_groups": 0, + "is_public": 0, + "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--" + }), + "type": "Bar", + "custom_options": json.dumps({ + "type": "bar", + "barOptions": { "stacked": 1 }, + "axisOptions": { "shortenYAxisNumbers": 1 }, + "tooltipOptions": {} + }), + "doctype": "Dashboard Chart", + "y_axis": [] + }, + { + "name": "Category-wise Asset Value", + "chart_name": _("Category-wise Asset Value"), + "chart_type": "Report", + "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 + }), + "type": "Donut", + "doctype": "Dashboard Chart", + "y_axis": [ + { + "parent": "Category-wise Asset Value", + "parentfield": "y_axis", + "parenttype": "Dashboard Chart", + "y_field": "asset_value", + "doctype": "Dashboard Chart Field" + } + ], + "custom_options": json.dumps({ + "type": "donut", + "height": 300, + "axisOptions": {"shortenYAxisNumbers": 1} + }) + }, + { + "name": "Location-wise Asset Value", + "chart_name": "Location-wise Asset Value", + "chart_type": "Report", + "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 + }), + "type": "Donut", + "doctype": "Dashboard Chart", + "y_axis": [ + { + "parent": "Location-wise Asset Value", + "parentfield": "y_axis", + "parenttype": "Dashboard Chart", + "y_field": "asset_value", + "doctype": "Dashboard Chart Field" + } + ], + "custom_options": json.dumps({ + "type": "donut", + "height": 300, + "axisOptions": {"shortenYAxisNumbers": 1} + }) + } + ] + +def get_number_cards(fiscal_year, year_start_date, year_end_date): + return [ + { + "name": "Total Assets", + "label": _("Total Assets"), + "function": "Count", + "document_type": "Asset", + "is_public": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly", + "filters_json": "[]", + "doctype": "Number Card", + }, + { + "name": "New Assets (This Year)", + "label": _("New Assets (This Year)"), + "function": "Count", + "document_type": "Asset", + "is_public": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly", + "filters_json": json.dumps([ + ['Asset', 'creation', 'between', [year_start_date, year_end_date]] + ]), + "doctype": "Number Card", + }, + { + "name": "Asset Value", + "label": _("Asset Value"), + "function": "Sum", + "aggregate_function_based_on": "value_after_depreciation", + "document_type": "Asset", + "is_public": 1, + "show_percentage_stats": 1, + "stats_time_interval": "Monthly", + "filters_json": "[]", + "doctype": "Number Card" + } + ] \ No newline at end of file diff --git a/erpnext/assets/desk_page/assets/assets.json b/erpnext/assets/desk_page/assets/assets.json index 03094160e5..449a5facb0 100644 --- a/erpnext/assets/desk_page/assets/assets.json +++ b/erpnext/assets/desk_page/assets/assets.json @@ -17,21 +17,27 @@ } ], "category": "Modules", - "charts": [], + "charts": [ + { + "chart_name": "Asset Value Analytics", + "label": "Asset Value Analytics" + } + ], "creation": "2020-03-02 15:43:27.634865", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Assets", - "modified": "2020-04-01 11:28:51.072198", + "modified": "2020-05-20 18:05:23.994795", "modified_by": "Administrator", "module": "Assets", "name": "Assets", + "onboarding": "Assets", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, @@ -42,14 +48,19 @@ "type": "DocType" }, { - "label": "Asset Movement", - "link_to": "Asset Movement", + "label": "Asset Category", + "link_to": "Asset Category", "type": "DocType" }, { "label": "Fixed Asset Register", "link_to": "Fixed Asset Register", "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "Asset", + "type": "Dashboard" } ] } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index a53ff88177..fba20c0c87 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -387,7 +387,8 @@ frappe.ui.form.on('Asset', { } frm.set_value('gross_purchase_amount', item.base_net_rate + item.item_tax_amount); frm.set_value('purchase_receipt_amount', item.base_net_rate + item.item_tax_amount); - frm.set_value('location', item.asset_location); + item.asset_location && frm.set_value('location', item.asset_location); + frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center); }, set_depreciation_rate: function(frm, row) { diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 505ba4c6b6..0bd03a8dbe 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -35,11 +35,9 @@ class Asset(AccountsController): if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() - def before_cancel(self): - self.cancel_auto_gen_movement() - def on_cancel(self): self.validate_cancellation() + self.cancel_movement_entries() self.delete_depreciation_entries() self.set_status() self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') @@ -127,24 +125,13 @@ class Asset(AccountsController): 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.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount: frappe.throw(_("Gross Purchase Amount should be {} to purchase amount of one single Asset. {}\ Please do not book expense of multiple assets against one single Asset.") .format(frappe.bold("equal"), "
"), title=_("Invalid Gross Purchase Amount")) - def cancel_auto_gen_movement(self): - 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) - if len(movements) > 1: - frappe.throw(_('Asset has multiple Asset Movement Entries which has to be \ - cancelled manually to cancel this asset.')) - if movements: - movement = frappe.get_doc('Asset Movement', movements[0].get('name')) - movement.flags.ignore_validate = True - movement.cancel() - def make_asset_movement(self): reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice' reference_docname = self.purchase_receipt or self.purchase_invoice @@ -406,9 +393,21 @@ class Asset(AccountsController): 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.")) if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"): frappe.throw(_("Asset cannot be cancelled, as it is already {0}").format(self.status)) + def cancel_movement_entries(self): + 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) + + for movement in movements: + movement = frappe.get_doc('Asset Movement', movement.get('name')) + movement.cancel() + def delete_depreciation_entries(self): for d in self.get("schedules"): if d.journal_entry: diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 522c1fef67..8f0afb42b2 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -10,7 +10,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g def post_depreciation_entries(date=None): # Return if automatic booking of asset depreciation is disabled - if not cint(frappe.db.get_single_value("Accounts Settings", "book_asset_depreciation_entry_automatically")): + if not cint(frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically")): return if not date: @@ -58,7 +58,8 @@ def make_depreciation_entry(asset_name, date=None): "account": accumulated_depreciation_account, "credit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", - "reference_name": asset.name + "reference_name": asset.name, + "cost_center": "" } debit_entry = { @@ -196,12 +197,14 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None) { "account": fixed_asset_account, "credit_in_account_currency": asset.gross_purchase_amount, - "credit": asset.gross_purchase_amount + "credit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center }, { "account": accumulated_depr_account, "debit_in_account_currency": accumulated_depr_amount, - "debit": accumulated_depr_amount + "debit": accumulated_depr_amount, + "cost_center": depreciation_cost_center } ] diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json index efb2de3f26..c0c2566fe2 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json @@ -1,559 +1,140 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:asset_name", - "beta": 0, - "creation": "2017-10-19 16:50:22.879545", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "field:asset_name", + "creation": "2017-10-19 16:50:22.879545", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "asset_name", + "asset_category", + "company", + "column_break_3", + "item_code", + "item_name", + "section_break_6", + "maintenance_team", + "column_break_9", + "maintenance_manager", + "maintenance_manager_name", + "section_break_8", + "asset_maintenance_tasks" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "asset_name", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Asset Name", - "length": 0, - "no_copy": 0, - "options": "Asset", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "asset_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset Name", + "options": "Asset", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "asset_name.asset_category", - "fieldname": "asset_category", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Asset Category", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "asset_category", + "fieldtype": "Read Only", + "label": "Asset Category" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "asset_name.item_code", - "fieldname": "item_code", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Read Only", + "label": "Item Code" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "asset_name.item_name", - "fieldname": "item_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Read Only", + "label": "Item Name" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "select_serial_no", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Select Serial No", - "length": 0, - "no_copy": 0, - "options": "Serial No", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "serial_no", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Serial No", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "maintenance_team", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Maintenance Team", + "options": "Asset Maintenance Team", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maintenance_team", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Maintenance Team", - "length": 0, - "no_copy": 0, - "options": "Asset Maintenance Team", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "maintenance_team.maintenance_manager", - "fieldname": "maintenance_manager", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Maintenance Manager", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "maintenance_manager", + "fieldtype": "Data", + "hidden": 1, + "label": "Maintenance Manager", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "maintenance_team.maintenance_manager_name", - "fieldname": "maintenance_manager_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Maintenance Manager Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "maintenance_manager_name", + "fieldtype": "Read Only", + "label": "Maintenance Manager Name" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_8", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Tasks", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "label": "Tasks" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "asset_maintenance_tasks", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Maintenance Tasks", - "length": 0, - "no_copy": 0, - "options": "Asset Maintenance Task", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "asset_maintenance_tasks", + "fieldtype": "Table", + "label": "Maintenance Tasks", + "options": "Asset Maintenance Task", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-05-22 17:20:54.711885", - "modified_by": "Administrator", - "module": "Assets", - "name": "Asset Maintenance", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-05-28 20:28:32.993823", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Maintenance", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Quality Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 3f046113a0..1869a29c8d 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -38,10 +38,10 @@ class AssetMaintenance(Document): @frappe.whitelist() def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, next_due_date): - team_member = frappe.get_doc('User', assign_to_member).email + team_member = frappe.db.get_value('User', assign_to_member, "email") args = { 'doctype' : 'Asset Maintenance', - 'assign_to' : team_member, + 'assign_to' : [team_member], 'name' : asset_maintenance_name, 'description' : maintenance_task, 'date' : next_due_date @@ -77,7 +77,7 @@ def calculate_next_due_date(periodicity, start_date = None, end_date = None, las 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.maintenance_task, "maintenance_status": ('in',['Planned','Overdue'])}) + "task": task.name, "maintenance_status": ('in',['Planned','Overdue'])}) if not asset_maintenance_log: asset_maintenance_log = frappe.get_doc({ @@ -86,7 +86,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task): "asset_name": asset_maintenance, "item_code": item_code, "item_name": item_name, - "task": task.maintenance_task, + "task": task.name, "has_certificate": task.certificate_required, "description": task.description, "assign_to_name": task.assign_to_name, diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json index 42aecdf44a..7395bec1e6 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json @@ -1,819 +1,210 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2017-10-23 16:58:44.424309", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "naming_series:", + "creation": "2017-10-23 16:58:44.424309", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "asset_maintenance", + "naming_series", + "asset_name", + "column_break_2", + "item_code", + "item_name", + "section_break_5", + "task", + "task_name", + "maintenance_type", + "periodicity", + "assign_to_name", + "column_break_6", + "due_date", + "completion_date", + "maintenance_status", + "section_break_12", + "has_certificate", + "certificate_attachement", + "section_break_6", + "description", + "column_break_9", + "actions_performed", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "asset_maintenance", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Asset Maintenance", - "length": 0, - "no_copy": 0, - "options": "Asset Maintenance", - "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": "asset_maintenance", + "fieldtype": "Link", + "label": "Asset Maintenance", + "options": "Asset Maintenance" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Series", - "length": 0, - "no_copy": 0, - "options": "ACC-AML-.YYYY.-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "ACC-AML-.YYYY.-", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "asset_maintenance.asset_name", - "fieldname": "asset_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Asset Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "asset_maintenance.asset_name", + "fieldname": "asset_name", + "fieldtype": "Read Only", + "label": "Asset Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "asset_maintenance.item_code", - "fieldname": "item_code", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "asset_maintenance.item_code", + "fieldname": "item_code", + "fieldtype": "Read Only", + "label": "Item Code" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "asset_maintenance.item_name", - "fieldname": "item_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "asset_maintenance.item_name", + "fieldname": "item_name", + "fieldtype": "Read Only", + "label": "Item Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "task", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Task", - "length": 0, - "no_copy": 0, - "options": "Asset Maintenance Task", - "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": "task", + "fieldtype": "Link", + "label": "Task", + "options": "Asset Maintenance Task" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "task.maintenance_type", - "fieldname": "maintenance_type", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Maintenance Type", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "task.maintenance_type", + "fieldname": "maintenance_type", + "fieldtype": "Read Only", + "label": "Maintenance Type" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "task.periodicity", - "fieldname": "periodicity", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Periodicity", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "task.periodicity", + "fieldname": "periodicity", + "fieldtype": "Data", + "label": "Periodicity", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "task.assign_to_name", - "fieldname": "assign_to_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Assign To", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "task.assign_to_name", + "fieldname": "assign_to_name", + "fieldtype": "Read Only", + "label": "Assign To" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "task.next_due_date", - "fieldname": "due_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Due Date", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "task.next_due_date", + "fieldname": "due_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Due Date", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "completion_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Completion Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "completion_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Completion Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maintenance_status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Maintenance Status", - "length": 0, - "no_copy": 0, - "options": "Planned\nCompleted\nCancelled\nOverdue", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "maintenance_status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Maintenance Status", + "options": "Planned\nCompleted\nCancelled\nOverdue", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_12", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_12", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "task.certificate_required", - "fieldname": "has_certificate", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Has Certificate ", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fetch_from": "task.certificate_required", + "fieldname": "has_certificate", + "fieldtype": "Check", + "label": "Has Certificate " + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.has_certificate", - "fieldname": "certificate_attachement", - "fieldtype": "Attach", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Certificate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.has_certificate", + "fieldname": "certificate_attachement", + "fieldtype": "Attach", + "label": "Certificate" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "task.description", - "fieldname": "description", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "task.description", + "fieldname": "description", + "fieldtype": "Read Only", + "label": "Description", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "actions_performed", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Actions performed", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "fieldname": "actions_performed", + "fieldtype": "Text Editor", + "label": "Actions performed" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Asset Maintenance Log", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Asset Maintenance Log", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "task.maintenance_task", + "fieldname": "task_name", + "fieldtype": "Data", + "in_preview": 1, + "label": "Task Name", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 14:44:51.457835", - "modified_by": "Administrator", - "module": "Assets", - "name": "Asset Maintenance Log", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2020-05-28 20:51:48.238397", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Maintenance Log", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "", - "track_changes": 1, - "track_seen": 1, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 1 } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.py b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.py index d98cc310b3..2a5666d506 100644 --- a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.py +++ b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.py @@ -7,5 +7,4 @@ import frappe from frappe.model.document import Document class AssetMaintenanceTask(Document): - def autoname(self): - self.name = self.maintenance_task + pass diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 3da355e2b9..b2de250b16 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -87,33 +87,9 @@ class AssetMovement(Document): def on_submit(self): self.set_latest_location_in_asset() - - def before_cancel(self): - self.validate_last_movement() def on_cancel(self): self.set_latest_location_in_asset() - - def validate_last_movement(self): - for d in self.assets: - auto_gen_movement_entry = frappe.db.sql( - """ - SELECT asm.name - FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm - WHERE - asm.docstatus=1 and - asm_item.parent=asm.name and - asm_item.asset=%s and - asm.company=%s and - asm_item.source_location is NULL and - asm.purpose=%s - ORDER BY - asm.transaction_date asc - """, (d.asset, self.company, 'Receipt'), as_dict=1) - - if auto_gen_movement_entry and auto_gen_movement_entry[0].get('name') == self.name: - frappe.throw(_('{0} will be cancelled automatically on asset cancellation as it was \ - auto generated for Asset {1}').format(self.name, d.asset)) def set_latest_location_in_asset(self): current_location, current_employee = '', '' diff --git a/erpnext/assets/module_onboarding/assets/assets.json b/erpnext/assets/module_onboarding/assets/assets.json new file mode 100644 index 0000000000..1086ab4bcd --- /dev/null +++ b/erpnext/assets/module_onboarding/assets/assets.json @@ -0,0 +1,41 @@ +{ + "allow_roles": [ + { + "role": "Accounts User" + }, + { + "role": "Maintenance User" + } + ], + "creation": "2020-05-08 15:10:45.571457", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/asset", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-08 14:05:51.828497", + "modified_by": "Administrator", + "module": "Assets", + "name": "Assets", + "owner": "Administrator", + "steps": [ + { + "step": "Introduction to Assets" + }, + { + "step": "Create a Fixed Asset Item" + }, + { + "step": "Create an Asset Category" + }, + { + "step": "Purchase an Asset Item" + }, + { + "step": "Create an Asset" + } + ], + "subtitle": "Assets, Depreciations, Repairs, and more.", + "success_message": "The Assets Module is all set up!", + "title": "Let's Set Up the Assets Module." +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/create_a_fixed_asset_item/create_a_fixed_asset_item.json b/erpnext/assets/onboarding_step/create_a_fixed_asset_item/create_a_fixed_asset_item.json new file mode 100644 index 0000000000..51702d9cb5 --- /dev/null +++ b/erpnext/assets/onboarding_step/create_a_fixed_asset_item/create_a_fixed_asset_item.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-08 13:20:00.259985", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-08 13:20:00.259985", + "modified_by": "Administrator", + "name": "Create a Fixed Asset Item", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create a Fixed Asset Item", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/create_an_asset/create_an_asset.json b/erpnext/assets/onboarding_step/create_an_asset/create_an_asset.json new file mode 100644 index 0000000000..b4f8a05e37 --- /dev/null +++ b/erpnext/assets/onboarding_step/create_an_asset/create_an_asset.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-08 13:21:53.332538", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-08 13:21:53.332538", + "modified_by": "Administrator", + "name": "Create an Asset", + "owner": "Administrator", + "reference_document": "Asset", + "show_full_form": 0, + "title": "Create an Asset", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/create_an_asset_category/create_an_asset_category.json b/erpnext/assets/onboarding_step/create_an_asset_category/create_an_asset_category.json new file mode 100644 index 0000000000..ffdb954b95 --- /dev/null +++ b/erpnext/assets/onboarding_step/create_an_asset_category/create_an_asset_category.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-08 13:21:53.332538", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-08 13:21:53.332538", + "modified_by": "Administrator", + "name": "Create an Asset Category", + "owner": "Administrator", + "reference_document": "Asset Category", + "show_full_form": 0, + "title": "Create an Asset Category", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/introduction_to_assets/introduction_to_assets.json b/erpnext/assets/onboarding_step/introduction_to_assets/introduction_to_assets.json new file mode 100644 index 0000000000..d89da27197 --- /dev/null +++ b/erpnext/assets/onboarding_step/introduction_to_assets/introduction_to_assets.json @@ -0,0 +1,19 @@ +{ + "action": "Watch Video", + "creation": "2020-05-08 13:18:25.424715", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-08 16:06:16.625646", + "modified_by": "Administrator", + "name": "Introduction to Assets", + "owner": "Administrator", + "show_full_form": 0, + "title": "Introduction to Assets", + "validate_action": 0, + "video_url": "https://www.youtube.com/watch?v=I-K8pLRmvSo" +} \ No newline at end of file diff --git a/erpnext/assets/onboarding_step/purchase_an_asset_item/purchase_an_asset_item.json b/erpnext/assets/onboarding_step/purchase_an_asset_item/purchase_an_asset_item.json new file mode 100644 index 0000000000..ce3185ef44 --- /dev/null +++ b/erpnext/assets/onboarding_step/purchase_an_asset_item/purchase_an_asset_item.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-08 13:21:28.208059", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-08 13:21:28.208059", + "modified_by": "Administrator", + "name": "Purchase an Asset Item", + "owner": "Administrator", + "reference_document": "Purchase Receipt", + "show_full_form": 0, + "title": "Purchase an Asset Item", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index 91ce9ce7fe..1a6ef54a83 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -21,20 +21,54 @@ frappe.query_reports["Fixed Asset Register"] = { reqd: 1 }, { - fieldname:"purchase_date", - label: __("Purchase Date"), - fieldtype: "Date" + "fieldname":"filter_based_on", + "label": __("Period Based On"), + "fieldtype": "Select", + "options": ["Fiscal Year", "Date Range"], + "default": ["Fiscal Year"], + "reqd": 1 }, { - fieldname:"available_for_use_date", - label: __("Available For Use Date"), - fieldtype: "Date" + "fieldname":"from_date", + "label": __("Start Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12), + "depends_on": "eval: doc.filter_based_on == 'Date Range'", + "reqd": 1 }, { - fieldname:"finance_book", - label: __("Finance Book"), - fieldtype: "Link", - options: "Finance Book" + "fieldname":"to_date", + "label": __("End Date"), + "fieldtype": "Date", + "default": frappe.datetime.nowdate(), + "depends_on": "eval: doc.filter_based_on == 'Date Range'", + "reqd": 1 + }, + { + "fieldname":"from_fiscal_year", + "label": __("Start Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", + "reqd": 1 + }, + { + "fieldname":"to_fiscal_year", + "label": __("End Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", + "reqd": 1 + }, + { + "fieldname":"date_based_on", + "label": __("Date Based On"), + "fieldtype": "Select", + "options": ["Purchase Date", "Available For Use Date"], + "default": "Purchase Date", + "reqd": 1 }, { fieldname:"asset_category", @@ -42,6 +76,26 @@ frappe.query_reports["Fixed Asset Register"] = { fieldtype: "Link", options: "Asset Category" }, + { + fieldname:"finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book" + }, + { + fieldname:"cost_center", + label: __("Cost Center"), + fieldtype: "Link", + options: "Cost Center" + }, + { + fieldname:"group_by", + label: __("Group By"), + fieldtype: "Select", + options: ["--Select a group--", "Asset Category", "Location"], + default: "--Select a group--", + reqd: 1 + }, { fieldname:"is_existing_asset", label: __("Is Existing Asset"), 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 fa2fe7b4a3..af08a2a601 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -4,122 +4,39 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cstr, today, flt +from frappe.utils import cstr, today, flt, add_years, formatdate, getdate +from erpnext.accounts.report.financial_statements import get_period_list, get_fiscal_year_data, validate_fiscal_year def execute(filters=None): filters = frappe._dict(filters or {}) columns = get_columns(filters) data = get_data(filters) - return columns, data + chart = prepare_chart_data(data, filters) if filters.get("group_by") not in ("Asset Category", "Location") else {} -def get_columns(filters): - return [ - { - "label": _("Asset Id"), - "fieldtype": "Link", - "fieldname": "asset_id", - "options": "Asset", - "width": 100 - }, - { - "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": 90 - }, - { - "label": _("Purchase Date"), - "fieldtype": "Date", - "fieldname": "purchase_date", - "width": 90 - }, - { - "label": _("Available For Use Date"), - "fieldtype": "Date", - "fieldname": "available_for_use_date", - "width": 90 - }, - { - "label": _("Gross Purchase Amount"), - "fieldname": "gross_purchase_amount", - "options": "Currency", - "width": 90 - }, - { - "label": _("Asset Value"), - "fieldname": "asset_value", - "options": "Currency", - "width": 90 - }, - { - "label": _("Opening Accumulated Depreciation"), - "fieldname": "opening_accumulated_depreciation", - "options": "Currency", - "width": 90 - }, - { - "label": _("Depreciated Amount"), - "fieldname": "depreciated_amount", - "options": "Currency", - "width": 90 - }, - { - "label": _("Cost Center"), - "fieldtype": "Link", - "fieldname": "cost_center", - "options": "Cost Center", - "width": 100 - }, - { - "label": _("Department"), - "fieldtype": "Link", - "fieldname": "department", - "options": "Department", - "width": 100 - }, - { - "label": _("Vendor Name"), - "fieldtype": "Data", - "fieldname": "vendor_name", - "width": 100 - }, - { - "label": _("Location"), - "fieldtype": "Link", - "fieldname": "location", - "options": "Location", - "width": 100 - }, - ] + return columns, data, None, chart def get_conditions(filters): conditions = { 'docstatus': 1 } status = filters.status - date = filters.date + date_field = frappe.scrub(filters.date_based_on or "Purchase Date") if filters.get('company'): conditions["company"] = filters.company - if filters.get('purchase_date'): - conditions["purchase_date"] = ('<=', filters.get('purchase_date')) - if filters.get('available_for_use_date'): - conditions["available_for_use_date"] = ('<=', filters.get('available_for_use_date')) + if filters.filter_based_on == "Date Range": + conditions[date_field] = ["between", [filters.from_date, filters.to_date]] + if filters.filter_based_on == "Fiscal Year": + fiscal_year = get_fiscal_year_data(filters.from_fiscal_year, filters.to_fiscal_year) + validate_fiscal_year(fiscal_year, filters.from_fiscal_year, filters.to_fiscal_year) + filters.year_start_date = getdate(fiscal_year.year_start_date) + 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') # In Store assets are those that are not sold or scrapped operand = 'not in' @@ -139,18 +56,28 @@ def get_data(filters): pr_supplier_map = get_purchase_receipt_supplier_map() pi_supplier_map = get_purchase_invoice_supplier_map() - assets_record = frappe.db.get_all("Asset", - filters=conditions, - fields=["name", "asset_name", "department", "cost_center", "purchase_receipt", + group_by = frappe.scrub(filters.get("group_by")) + + if group_by == "asset_category": + fields = ["asset_category", "gross_purchase_amount", "opening_accumulated_depreciation"] + assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields, group_by=group_by) + + elif group_by == "location": + fields = ["location", "gross_purchase_amount", "opening_accumulated_depreciation"] + 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", "status", "purchase_invoice", "opening_accumulated_depreciation"]) + "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) \ - flt(depreciation_amount_map.get(asset.name)) if asset_value: row = { - "asset_id": asset.name, + "asset_id": asset.asset_id, "asset_name": asset.asset_name, "status": asset.status, "department": asset.department, @@ -158,7 +85,7 @@ def get_data(filters): "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.name) or 0.0, + "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0, "available_for_use_date": asset.available_for_use_date, "location": asset.location, "asset_category": asset.asset_category, @@ -169,8 +96,39 @@ def get_data(filters): 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) + + for d in period_list: + labels_values_map.setdefault(d.get('label'), frappe._dict({'asset_value': 0, 'depreciated_amount': 0})) + + for d in data: + date = d.get(date_field) + belongs_to_month = formatdate(date, "MMM YYYY") + + labels_values_map[belongs_to_month].asset_value += d.get("asset_value") + labels_values_map[belongs_to_month].depreciated_amount += d.get("depreciated_amount") + + return { + "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()] } + ] + }, + "type": "bar", + "barOptions": { + "stacked": 1 + }, + } + def get_finance_book_value_map(filters): - date = filters.get('purchase_date') or filters.get('available_for_use_date') or today() + date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date return frappe._dict(frappe.db.sql(''' Select parent, SUM(depreciation_amount) @@ -201,3 +159,139 @@ def get_purchase_invoice_supplier_map(): AND pii.is_fixed_asset=1 AND pi.docstatus=1 AND pi.is_return=0''')) + +def get_columns(filters): + if filters.get("group_by") in ["Asset Category", "Location"]: + return [ + { + "label": _("{}").format(filters.get("group_by")), + "fieldtype": "Link", + "fieldname": frappe.scrub(filters.get("group_by")), + "options": filters.get("group_by"), + "width": 120 + }, + { + "label": _("Gross Purchase Amount"), + "fieldname": "gross_purchase_amount", + "fieldtype": "Currency", + "options": "company:currency", + "width": 100 + }, + { + "label": _("Opening Accumulated Depreciation"), + "fieldname": "opening_accumulated_depreciation", + "fieldtype": "Currency", + "options": "company:currency", + "width": 90 + }, + { + "label": _("Depreciated Amount"), + "fieldname": "depreciated_amount", + "fieldtype": "Currency", + "options": "company:currency", + "width": 100 + }, + { + "label": _("Asset Value"), + "fieldname": "asset_value", + "fieldtype": "Currency", + "options": "company:currency", + "width": 100 + } + ] + + return [ + { + "label": _("Asset Id"), + "fieldtype": "Link", + "fieldname": "asset_id", + "options": "Asset", + "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 + }, + { + "label": _("Available For Use Date"), + "fieldtype": "Date", + "fieldname": "available_for_use_date", + "width": 90 + }, + { + "label": _("Gross Purchase Amount"), + "fieldname": "gross_purchase_amount", + "fieldtype": "Currency", + "options": "company:currency", + "width": 100 + }, + { + "label": _("Asset Value"), + "fieldname": "asset_value", + "fieldtype": "Currency", + "options": "company:currency", + "width": 100 + }, + { + "label": _("Opening Accumulated Depreciation"), + "fieldname": "opening_accumulated_depreciation", + "fieldtype": "Currency", + "options": "company:currency", + "width": 90 + }, + { + "label": _("Depreciated Amount"), + "fieldname": "depreciated_amount", + "fieldtype": "Currency", + "options": "company:currency", + "width": 100 + }, + { + "label": _("Cost Center"), + "fieldtype": "Link", + "fieldname": "cost_center", + "options": "Cost Center", + "width": 100 + }, + { + "label": _("Department"), + "fieldtype": "Link", + "fieldname": "department", + "options": "Department", + "width": 100 + }, + { + "label": _("Vendor Name"), + "fieldtype": "Data", + "fieldname": "vendor_name", + "width": 100 + }, + { + "label": _("Location"), + "fieldtype": "Link", + "fieldname": "location", + "options": "Location", + "width": 100 + }, + ] \ No newline at end of file diff --git a/erpnext/buying/dashboard_fixtures.py b/erpnext/buying/dashboard_fixtures.py new file mode 100644 index 0000000000..c6e2ffa634 --- /dev/null +++ b/erpnext/buying/dashboard_fixtures.py @@ -0,0 +1,211 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import json +from frappe import _ +from frappe.utils import nowdate +from erpnext.accounts.dashboard_fixtures import _get_fiscal_year + +def get_data(): + + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + + company = frappe.get_doc("Company", get_company_for_dashboards()) + fiscal_year_name = fiscal_year.get("name") + start_date = str(fiscal_year.get("year_start_date")) + end_date = str(fiscal_year.get("year_end_date")) + + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(company, fiscal_year_name, start_date, end_date), + "number_cards": get_number_cards(company, fiscal_year_name, start_date, end_date), + }) + +def get_company_for_dashboards(): + company = frappe.defaults.get_defaults().company + if company: + return company + else: + company_list = frappe.get_list("Company") + if company_list: + return company_list[0].name + return None + +def get_dashboards(): + return [{ + "name": "Buying", + "dashboard_name": "Buying", + "charts": [ + { "chart": "Purchase Order Trends", "width": "Full"}, + { "chart": "Material Request Analysis", "width": "Half"}, + { "chart": "Purchase Order Analysis", "width": "Half"}, + { "chart": "Top Suppliers", "width": "Full"} + ], + "cards": [ + { "card": "Annual Purchase"}, + { "card": "Purchase Orders to Receive"}, + { "card": "Purchase Orders to Bill"}, + { "card": "Active Suppliers"} + ] + }] + +def get_charts(company, fiscal_year_name, start_date, end_date): + return [ + { + "name": "Purchase Order Analysis", + "chart_name": _("Purchase Order Analysis"), + "chart_type": "Report", + "custom_options": json.dumps({ + "type": "donut", + "height": 300, + "axisOptions": {"shortenYAxisNumbers": 1} + }), + "doctype": "Dashboard Chart", + "filters_json": json.dumps({ + "company": company.name, + "from_date": start_date, + "to_date": end_date + }), + "is_custom": 1, + "is_public": 1, + "owner": "Administrator", + "report_name": "Purchase Order Analysis", + "type": "Donut" + }, + { + "name": "Material Request Analysis", + "chart_name": _("Material Request Analysis"), + "chart_type": "Group By", + "custom_options": json.dumps({"height": 300}), + "doctype": "Dashboard Chart", + "document_type": "Material Request", + "filters_json": json.dumps( + [["Material Request", "status", "not in", ["Draft", "Cancelled", "Stopped", None], False], + ["Material Request", "material_request_type", "=", "Purchase", False], + ["Material Request", "company", "=", company.name, False], + ["Material Request", "docstatus", "=", 1, False], + ["Material Request", "transaction_date", "Between", [start_date, end_date], False]] + ), + "group_by_based_on": "status", + "group_by_type": "Count", + "is_custom": 0, + "is_public": 1, + "number_of_groups": 0, + "owner": "Administrator", + "type": "Donut" + }, + { + "name": "Purchase Order Trends", + "chart_name": _("Purchase Order Trends"), + "chart_type": "Report", + "custom_options": json.dumps({ + "type": "line", + "axisOptions": {"shortenYAxisNumbers": 1}, + "tooltipOptions": {}, + "lineOptions": { + "regionFill": 1 + } + }), + "doctype": "Dashboard Chart", + "filters_json": json.dumps({ + "company": company.name, + "period": "Monthly", + "fiscal_year": fiscal_year_name, + "period_based_on": "posting_date", + "based_on": "Item" + }), + "is_custom": 1, + "is_public": 1, + "owner": "Administrator", + "report_name": "Purchase Order Trends", + "type": "Line" + }, + { + "name": "Top Suppliers", + "chart_name": _("Top Suppliers"), + "chart_type": "Report", + "doctype": "Dashboard Chart", + "filters_json": json.dumps({ + "company": company.name, + "period": "Monthly", + "fiscal_year": fiscal_year_name, + "period_based_on": "posting_date", + "based_on": "Supplier" + }), + "is_custom": 1, + "is_public": 1, + "owner": "Administrator", + "report_name": "Purchase Receipt Trends", + "type": "Bar" + } + ] + +def get_number_cards(company, fiscal_year_name, start_date, end_date): + return [ + { + "name": "Annual Purchase", + "aggregate_function_based_on": "base_net_total", + "doctype": "Number Card", + "document_type": "Purchase Order", + "filters_json": json.dumps([ + ["Purchase Order", "transaction_date", "Between", [start_date, end_date], False], + ["Purchase Order", "status", "not in", ["Draft", "Cancelled", "Closed", None], False], + ["Purchase Order", "docstatus", "=", 1, False], + ["Purchase Order", "company", "=", company.name, False] + ]), + "function": "Sum", + "is_public": 1, + "label": _("Annual Purchase"), + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "name": "Purchase Orders to Receive", + "doctype": "Number Card", + "document_type": "Purchase Order", + "filters_json": json.dumps([ + ["Purchase Order", "status", "in", ["To Receive and Bill", "To Receive", None], False], + ["Purchase Order", "docstatus", "=", 1, False], + ["Purchase Order", "company", "=", company.name, False] + ]), + "function": "Count", + "is_public": 1, + "label": _("Purchase Orders to Receive"), + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Weekly" + }, + { + "name": "Purchase Orders to Bill", + "doctype": "Number Card", + "document_type": "Purchase Order", + "filters_json": json.dumps([ + ["Purchase Order", "status", "in", ["To Receive and Bill", "To Bill", None], False], + ["Purchase Order", "docstatus", "=", 1, False], + ["Purchase Order", "company", "=", company.name, False] + ]), + "function": "Count", + "is_public": 1, + "label": _("Purchase Orders to Bill"), + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Weekly" + }, + { + "name": "Active Suppliers", + "doctype": "Number Card", + "document_type": "Supplier", + "filters_json": json.dumps([["Supplier", "disabled", "=", "0"]]), + "function": "Count", + "is_public": 1, + "label": "Active Suppliers", + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + } + ] \ No newline at end of file diff --git a/erpnext/buying/desk_page/buying/buying.json b/erpnext/buying/desk_page/buying/buying.json index 5e764cf8bb..565d39c3c8 100644 --- a/erpnext/buying/desk_page/buying/buying.json +++ b/erpnext/buying/desk_page/buying/buying.json @@ -2,24 +2,24 @@ "cards": [ { "hidden": 0, - "label": "Supplier", - "links": "[\n {\n \"description\": \"Supplier database.\",\n \"label\": \"Supplier\",\n \"name\": \"Supplier\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Supplier Group master.\",\n \"label\": \"Supplier Group\",\n \"name\": \"Supplier Group\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Contacts.\",\n \"label\": \"Contact\",\n \"name\": \"Contact\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Addresses.\",\n \"label\": \"Address\",\n \"name\": \"Address\",\n \"type\": \"doctype\"\n }\n]" + "label": "Buying", + "links": "[ \n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Request for purchase.\",\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"description\": \"Purchase Orders given to Suppliers.\",\n \"label\": \"Purchase Order\",\n \"name\": \"Purchase Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Invoice\",\n \"name\": \"Purchase Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"description\": \"Request for quotation.\",\n \"label\": \"Request for Quotation\",\n \"name\": \"Request for Quotation\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"description\": \"Quotations received from Suppliers.\",\n \"label\": \"Supplier Quotation\",\n \"name\": \"Supplier Quotation\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, - "label": "Purchasing", - "links": "[\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"description\": \"Purchase Orders given to Suppliers.\",\n \"label\": \"Purchase Order\",\n \"name\": \"Purchase Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Invoice\",\n \"name\": \"Purchase Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Request for purchase.\",\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"description\": \"Request for quotation.\",\n \"label\": \"Request for Quotation\",\n \"name\": \"Request for Quotation\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"description\": \"Quotations received from Suppliers.\",\n \"label\": \"Supplier Quotation\",\n \"name\": \"Supplier Quotation\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Items and Pricing", - "links": "[\n {\n \"description\": \"All Products or Services.\",\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Multiple Item prices.\",\n \"label\": \"Item Price\",\n \"name\": \"Item Price\",\n \"onboard\": 1,\n \"route\": \"#Report/Item Price\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Price List master.\",\n \"label\": \"Price List\",\n \"name\": \"Price List\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Bundle items at time of sale.\",\n \"label\": \"Product Bundle\",\n \"name\": \"Product Bundle\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tree of Item Groups.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Item Group\",\n \"link\": \"Tree/Item Group\",\n \"name\": \"Item Group\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Rules for applying different promotional schemes.\",\n \"label\": \"Promotional Scheme\",\n \"name\": \"Promotional Scheme\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Rules for applying pricing and discount.\",\n \"label\": \"Pricing Rule\",\n \"name\": \"Pricing Rule\",\n \"type\": \"doctype\"\n }\n]" + "label": "Items & Pricing", + "links": "[\n {\n \"description\": \"All Products or Services.\",\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Multiple Item prices.\",\n \"label\": \"Item Price\",\n \"name\": \"Item Price\",\n \"onboard\": 1,\n \"route\": \"#Report/Item Price\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Price List master.\",\n \"label\": \"Price List\",\n \"name\": \"Price List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Bundle items at time of sale.\",\n \"label\": \"Product Bundle\",\n \"name\": \"Product Bundle\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tree of Item Groups.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Item Group\",\n \"link\": \"Tree/Item Group\",\n \"name\": \"Item Group\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Rules for applying different promotional schemes.\",\n \"label\": \"Promotional Scheme\",\n \"name\": \"Promotional Scheme\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Rules for applying pricing and discount.\",\n \"label\": \"Pricing Rule\",\n \"name\": \"Pricing Rule\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, "label": "Settings", "links": "[\n {\n \"description\": \"Default settings for buying transactions.\",\n \"label\": \"Buying Settings\",\n \"name\": \"Buying Settings\",\n \"settings\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tax template for buying transactions.\",\n \"label\": \"Purchase Taxes and Charges Template\",\n \"name\": \"Purchase Taxes and Charges Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Template of terms or contract.\",\n \"label\": \"Terms and Conditions Template\",\n \"name\": \"Terms and Conditions\",\n \"type\": \"doctype\"\n }\n]" }, + { + "hidden": 0, + "label": "Supplier", + "links": "[\n {\n \"description\": \"Supplier database.\",\n \"label\": \"Supplier\",\n \"name\": \"Supplier\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Supplier Group master.\",\n \"label\": \"Supplier Group\",\n \"name\": \"Supplier Group\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Contacts.\",\n \"label\": \"Contact\",\n \"name\": \"Contact\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Addresses.\",\n \"label\": \"Address\",\n \"name\": \"Address\",\n \"type\": \"doctype\"\n }\n]" + }, { "hidden": 0, "label": "Supplier Scorecard", @@ -28,67 +28,86 @@ { "hidden": 0, "label": "Key Reports", - "links": "[\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Analytics\",\n \"name\": \"Purchase Analytics\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier-Wise Sales Analytics\",\n \"name\": \"Supplier-Wise Sales Analytics\",\n \"onboard\": 1,\n \"reference_doctype\": \"Stock Ledger Entry\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Order Trends\",\n \"name\": \"Purchase Order Trends\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Procurement Tracker\",\n \"name\": \"Procurement Tracker\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Requested Items To Be Ordered\",\n \"name\": \"Requested Items To Be Ordered\",\n \"onboard\": 1,\n \"reference_doctype\": \"Material Request\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Analytics\",\n \"name\": \"Purchase Analytics\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier-Wise Sales Analytics\",\n \"name\": \"Supplier-Wise Sales Analytics\",\n \"onboard\": 1,\n \"reference_doctype\": \"Stock Ledger Entry\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Requested Items to Order\",\n \"name\": \"Requested Items to Order\",\n \"onboard\": 1,\n \"reference_doctype\": \"Material Request\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Order Trends\",\n \"name\": \"Purchase Order Trends\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Procurement Tracker\",\n \"name\": \"Procurement Tracker\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, "label": "Other Reports", - "links": "[\n {\n \"is_query_report\": true,\n \"label\": \"Items To Be Requested\",\n \"name\": \"Items To Be Requested\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Item-wise Purchase History\",\n \"name\": \"Item-wise Purchase History\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Material Requests for which Supplier Quotations are not created\",\n \"name\": \"Material Requests for which Supplier Quotations are not created\",\n \"reference_doctype\": \"Material Request\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"reference_doctype\": \"Address\",\n \"route_options\": {\n \"party_type\": \"Supplier\"\n },\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"is_query_report\": true,\n \"label\": \"Items To Be Requested\",\n \"name\": \"Items To Be Requested\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Item-wise Purchase History\",\n \"name\": \"Item-wise Purchase History\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"reference_doctype\": \"Purchase Receipt\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"reference_doctype\": \"Purchase Invoice\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Raw Materials To Be Transferred\",\n \"name\": \"Subcontracted Raw Materials To Be Transferred\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Item To Be Received\",\n \"name\": \"Subcontracted Item To Be Received\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Quoted Item Comparison\",\n \"name\": \"Quoted Item Comparison\",\n \"onboard\": 1,\n \"reference_doctype\": \"Supplier Quotation\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Material Requests for which Supplier Quotations are not created\",\n \"name\": \"Material Requests for which Supplier Quotations are not created\",\n \"reference_doctype\": \"Material Request\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"reference_doctype\": \"Address\",\n \"route_options\": {\n \"party_type\": \"Supplier\"\n },\n \"type\": \"report\"\n }\n]" + }, + { + "hidden": 0, + "label": "Regional", + "links": "[\n {\n \"description\": \"Import Italian Purchase Invoices\",\n \"label\": \"Import Supplier Invoice\",\n \"name\": \"Import Supplier Invoice\",\n \"type\": \"doctype\"\n } \n]" } ], + "cards_label": "", "category": "Modules", "charts": [ { - "chart_name": "Expenses", - "label": "Expenses" + "chart_name": "Purchase Order Trends", + "label": "Purchase Order Trends" } ], + "charts_label": "", "creation": "2020-01-28 11:50:26.195467", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Buying", - "modified": "2020-04-01 11:28:51.192097", + "modified": "2020-06-29 19:30:24.983050", "modified_by": "Administrator", "module": "Buying", "name": "Buying", + "onboarding": "Buying", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "shortcuts": [ { - "format": "{} Unpaid", - "label": "Purchase Invoice", - "link_to": "Purchase Invoice", - "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"Unpaid\"\n}", + "color": "#cef6d1", + "format": "{} Available", + "label": "Item", + "link_to": "Item", + "stats_filter": "{\n \"disabled\": 0\n}", "type": "DocType" }, { - "format": "{} to receive", + "color": "#ffe8cd", + "format": "{} Pending", + "label": "Material Request", + "link_to": "Material Request", + "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"Pending\"\n}", + "type": "DocType" + }, + { + "color": "#ffe8cd", + "format": "{} To Receive", "label": "Purchase Order", "link_to": "Purchase Order", - "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"To Receive\"\n}", + "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\":[\"in\", [\"To Receive\", \"To Receive and Bill\"]]\n}", "type": "DocType" }, { - "label": "Supplier Quotation", - "link_to": "Supplier Quotation", - "type": "DocType" - }, - { - "label": "Accounts Payable", - "link_to": "Accounts Payable", + "label": "Purchase Analytics", + "link_to": "Purchase Analytics", "type": "Report" }, { - "label": "Purchase Register", - "link_to": "Purchase Register", + "label": "Purchase Order Analysis", + "link_to": "Purchase Order Analysis", "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "Buying", + "type": "Dashboard" } - ] + ], + "shortcuts_label": "" } \ No newline at end of file diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.js b/erpnext/buying/doctype/buying_settings/buying_settings.js index 403b1c95e7..e496e9628d 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.js +++ b/erpnext/buying/doctype/buying_settings/buying_settings.js @@ -6,3 +6,26 @@ frappe.ui.form.on('Buying Settings', { // } }); + +frappe.tour['Buying Settings'] = [ + { + fieldname: "supp_master_name", + title: "Supplier Naming By", + description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ") + "Naming Series" + __(" choose the 'Naming Series' option."), + }, + { + fieldname: "buying_price_list", + title: "Default Buying Price List", + description: __("Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.") + }, + { + fieldname: "po_required", + title: "Purchase Order Required for Purchase Invoice & Receipt Creation", + description: __("If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.") + }, + { + fieldname: "pr_required", + title: "Purchase Receipt Required for Purchase Invoice Creation", + description: __("If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.") + } +]; \ No newline at end of file diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index a492519591..a0ab2a00f9 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -1,8 +1,10 @@ { + "actions": [], "creation": "2013-06-25 11:04:03", "description": "Settings for Buying Module", "doctype": "DocType", "document_type": "Other", + "engine": "InnoDB", "field_order": [ "supp_master_name", "supplier_group", @@ -44,13 +46,13 @@ { "fieldname": "po_required", "fieldtype": "Select", - "label": "Purchase Order Required", + "label": "Purchase Order Required for Purchase Invoice & Receipt Creation", "options": "No\nYes" }, { "fieldname": "pr_required", "fieldtype": "Select", - "label": "Purchase Receipt Required", + "label": "Purchase Receipt Required for Purchase Invoice Creation", "options": "No\nYes" }, { @@ -92,7 +94,8 @@ "icon": "fa fa-cog", "idx": 1, "issingle": 1, - "modified": "2019-08-20 13:13:09.055189", + "links": [], + "modified": "2020-05-15 14:49:32.513611", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -107,5 +110,7 @@ "share": 1, "write": 1 } - ] + ], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 4a8146a797..84e3a31904 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -123,14 +123,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( } if(doc.status != "Closed") { if (doc.status != "On Hold") { - if(flt(doc.per_received, 2) < 100 && allow_receipt) { + if(flt(doc.per_received) < 100 && allow_receipt) { cur_frm.add_custom_button(__('Receipt'), this.make_purchase_receipt, __('Create')); if(doc.is_subcontracted==="Yes" && me.has_unsupplied_items()) { cur_frm.add_custom_button(__('Material to Supplier'), function() { me.make_stock_entry(); }, __("Transfer")); } } - if(flt(doc.per_billed, 2) < 100) + if(flt(doc.per_billed) < 100) cur_frm.add_custom_button(__('Invoice'), this.make_purchase_invoice, __('Create')); diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index a4f60fbba5..13f5cb0c81 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -38,6 +38,8 @@ "col_break_address", "shipping_address", "shipping_address_display", + "billing_address", + "billing_address_display", "currency_and_price_list", "currency", "conversion_rate", @@ -135,7 +137,9 @@ { "fieldname": "supplier_section", "fieldtype": "Section Break", - "options": "fa fa-user" + "options": "fa fa-user", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -145,7 +149,9 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "naming_series", @@ -157,7 +163,9 @@ "options": "PUR-ORD-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -170,14 +178,18 @@ "options": "Supplier", "print_hide": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.supplier && doc.docstatus===0 && (!(doc.items && doc.items.length) || (doc.items.length==1 && !doc.items[0].item_code))", "description": "Fetch items based on Default Supplier.", "fieldname": "get_items_from_open_material_requests", "fieldtype": "Button", - "label": "Get Items from Open Material Requests" + "label": "Get Items from Open Material Requests", + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -186,7 +198,9 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Supplier Name", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -198,13 +212,17 @@ "options": "Company", "print_hide": 1, "remember_last_selected_value": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_width": "50%", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -216,27 +234,35 @@ "oldfieldname": "transaction_date", "oldfieldtype": "Date", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "schedule_date", "fieldtype": "Date", - "label": "Required By" + "label": "Required By", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "depends_on": "eval:doc.docstatus===1", "fieldname": "order_confirmation_no", "fieldtype": "Data", - "label": "Order Confirmation No" + "label": "Order Confirmation No", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "depends_on": "eval:doc.order_confirmation_no", "fieldname": "order_confirmation_date", "fieldtype": "Date", - "label": "Order Confirmation Date" + "label": "Order Confirmation Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -248,19 +274,25 @@ "oldfieldtype": "Data", "options": "Purchase Order", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "drop_ship", "fieldtype": "Section Break", - "label": "Drop Ship" + "label": "Drop Ship", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer", "fieldtype": "Link", "label": "Customer", "options": "Customer", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -268,31 +300,41 @@ "fieldtype": "Data", "label": "Customer Name", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_19", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_contact_person", "fieldtype": "Link", "label": "Customer Contact", - "options": "Contact" + "options": "Contact", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_contact_display", "fieldtype": "Small Text", "hidden": 1, "label": "Customer Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_contact_mobile", "fieldtype": "Small Text", "hidden": 1, "label": "Customer Mobile No", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "customer_contact_email", @@ -300,46 +342,60 @@ "hidden": 1, "label": "Customer Contact Email", "options": "Email", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact" + "label": "Address and Contact", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplier_address", "fieldtype": "Link", "label": "Select Supplier Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "contact_person", "fieldtype": "Link", "label": "Contact Person", "options": "Contact", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", - "read_only": 1 + "read_only": 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", @@ -347,32 +403,42 @@ "label": "Contact Email", "options": "Email", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_address", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address", "fieldtype": "Link", "label": "Select Shipping Address", "options": "Address", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1 + "read_only": 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", @@ -382,7 +448,9 @@ "oldfieldtype": "Select", "options": "Currency", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "conversion_rate", @@ -392,18 +460,24 @@ "oldfieldtype": "Currency", "precision": "9", "print_hide": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cb_price_list", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "price_list_currency", @@ -411,14 +485,18 @@ "label": "Price List Currency", "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "plc_conversion_rate", "fieldtype": "Float", "label": "Price List Exchange Rate", "precision": "9", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -427,11 +505,15 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "description": "Sets 'Warehouse' in each row of the Items table.", @@ -439,11 +521,15 @@ "fieldtype": "Link", "label": "Set Target Warehouse", "options": "Warehouse", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -452,25 +538,33 @@ "in_standard_filter": 1, "label": "Supply Raw Materials", "options": "No\nYes", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.is_subcontracted==\"Yes\"", "fieldname": "supplier_warehouse", "fieldtype": "Link", "label": "Supplier Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "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 }, { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode" + "label": "Scan Barcode", + "show_days": 1, + "show_seconds": 1 }, { "allow_bulk_edit": 1, @@ -480,26 +574,34 @@ "oldfieldname": "po_details", "oldfieldtype": "Table", "options": "Purchase Order Item", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "section_break_48", "fieldtype": "Section Break", - "label": "Pricing Rules" + "label": "Pricing Rules", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Purchase Order Pricing Rule", "options": "Pricing Rule Detail", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible_depends_on": "supplied_items", "fieldname": "raw_material_details", "fieldtype": "Section Break", - "label": "Raw Materials Supplied" + "label": "Raw Materials Supplied", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "supplied_items", @@ -509,17 +611,23 @@ "oldfieldtype": "Table", "options": "Purchase Order Item Supplied", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sb_last_purchase", - "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", @@ -527,7 +635,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", @@ -538,18 +648,24 @@ "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_26", - "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", @@ -559,20 +675,26 @@ "oldfieldtype": "Currency", "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", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges", @@ -581,22 +703,30 @@ "oldfieldname": "purchase_other_charges", "oldfieldtype": "Link", "options": "Purchase Taxes and Charges Template", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_50", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "shipping_rule", "fieldtype": "Link", "label": "Shipping Rule", "options": "Shipping Rule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_52", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes", @@ -604,13 +734,17 @@ "label": "Purchase Taxes and Charges", "oldfieldname": "purchase_tax_details", "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges" + "options": "Purchase 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", @@ -619,13 +753,17 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money" + "options": "fa fa-money", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_added", @@ -635,7 +773,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_taxes_and_charges_deducted", @@ -645,7 +785,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -656,11 +798,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_39", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_added", @@ -670,7 +816,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "taxes_and_charges_deducted", @@ -680,7 +828,9 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_taxes_and_charges", @@ -688,14 +838,18 @@ "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": "discount_section", "fieldtype": "Section Break", - "label": "Additional Discount" + "label": "Additional Discount", + "show_days": 1, + "show_seconds": 1 }, { "default": "Grand Total", @@ -703,7 +857,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", @@ -711,28 +867,38 @@ "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_45", - "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_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_grand_total", @@ -743,7 +909,9 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_rounding_adjustment", @@ -752,7 +920,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 Purchase Order.", @@ -762,7 +932,9 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "base_rounded_total", @@ -772,12 +944,16 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break4", "fieldtype": "Column Break", - "oldfieldtype": "Column Break" + "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "grand_total", @@ -787,7 +963,9 @@ "oldfieldname": "grand_total_import", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rounding_adjustment", @@ -796,20 +974,26 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rounded_total", "fieldtype": "Currency", "label": "Rounded Total", "options": "currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total" + "label": "Disable Rounded Total", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "in_words", @@ -818,7 +1002,9 @@ "oldfieldname": "in_words_import", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "advance_paid", @@ -827,19 +1013,25 @@ "no_copy": 1, "options": "party_account_currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "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" + "options": "Payment Terms Template", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_schedule", @@ -847,7 +1039,9 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -856,7 +1050,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", @@ -865,21 +1061,27 @@ "oldfieldname": "tc_name", "oldfieldtype": "Link", "options": "Terms and Conditions", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", "label": "Terms and Conditions", "oldfieldname": "terms", - "oldfieldtype": "Text Editor" + "oldfieldtype": "Text Editor", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "more_info", "fieldtype": "Section Break", "label": "More Information", - "oldfieldtype": "Section Break" + "oldfieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "Draft", @@ -894,7 +1096,9 @@ "print_hide": 1, "read_only": 1, "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "ref_sq", @@ -905,7 +1109,9 @@ "oldfieldname": "ref_sq", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "party_account_currency", @@ -915,18 +1121,24 @@ "no_copy": 1, "options": "Currency", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "inter_company_order_reference", "fieldtype": "Link", "label": "Inter Company Order Reference", "options": "Sales Order", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_74", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.__islocal", @@ -936,7 +1148,9 @@ "label": "% Received", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:!doc.__islocal", @@ -946,7 +1160,9 @@ "label": "% Billed", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, @@ -956,6 +1172,8 @@ "oldfieldtype": "Column Break", "print_hide": 1, "print_width": "50%", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -966,7 +1184,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, @@ -978,11 +1198,15 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1 + "report_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_86", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -990,19 +1214,25 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", - "label": "Subscription Section" + "label": "Subscription Section", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1010,7 +1240,9 @@ "fieldtype": "Date", "label": "From Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -1018,11 +1250,15 @@ "fieldtype": "Date", "label": "To Date", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_97", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "auto_repeat", @@ -1031,44 +1267,72 @@ "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 }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", - "options": "Tax Category" + "options": "Tax Category", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "supplied_items", "fieldname": "set_reserve_warehouse", "fieldtype": "Link", "label": "Set Reserve Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "tracking_section", "fieldtype": "Section Break", - "label": "Tracking" + "label": "Tracking", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_75", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Select Billing Address", + "options": "Address", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-04-24 12:13:14.186280", + "modified": "2020-06-13 22:25:47.333850", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", @@ -1112,6 +1376,12 @@ "read": 1, "role": "Purchase Manager", "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "role": "Accounts User" } ], "search_fields": "status, transaction_date, supplier,grand_total", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index f62df20ae1..c7efb8a1a1 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -71,6 +71,15 @@ class PurchaseOrder(BuyingController): "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 } }) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 1712369e60..813286f7fa 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -118,7 +118,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(po.get("items")[0].amount, 1400) self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3) - + def test_add_new_item_in_update_child_qty_rate(self): po = create_purchase_order(do_not_save=1) po.items[0].qty = 4 @@ -144,7 +144,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEquals(len(po.get('items')), 2) self.assertEqual(po.status, 'To Receive and Bill') - + def test_remove_item_in_update_child_qty_rate(self): po = create_purchase_order(do_not_save=1) po.items[0].qty = 4 @@ -185,6 +185,23 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEquals(len(po.get('items')), 1) self.assertEqual(po.status, 'To Receive and Bill') + def test_update_child_qty_rate_perm(self): + po = create_purchase_order(item_code= "_Test Item", qty=4) + + 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) + + # 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) + frappe.set_user("Administrator") + def test_update_qty(self): po = create_purchase_order() @@ -689,7 +706,7 @@ class TestPurchaseOrder(unittest.TestCase): 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. diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 455bd68ecf..4a937f7f0d 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -166,7 +166,8 @@ frappe.ui.form.on("Request for Quotation",{ { "fieldtype": "Select", "label": __("Supplier"), "fieldname": "supplier", "options": doc.suppliers.map(d => d.supplier), - "reqd": 1 }, + "reqd": 1, + "default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" }, { "fieldtype": "Button", "label": __('Create Supplier Quotation'), "fieldname": "make_supplier_quotation", "cssClass": "btn-primary" }, ] diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 97aa9221e2..5cd8e6f4fa 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -1,4 +1,5 @@ { + "actions": "", "allow_import": 1, "autoname": "naming_series:", "creation": "2016-02-25 01:24:07.224790", @@ -28,7 +29,6 @@ "letter_head", "more_info", "status", - "fiscal_year", "column_break3", "amended_from" ], @@ -218,17 +218,6 @@ "reqd": 1, "search_index": 1 }, - { - "fieldname": "fiscal_year", - "fieldtype": "Link", - "label": "Fiscal Year", - "oldfieldname": "fiscal_year", - "oldfieldtype": "Select", - "options": "Fiscal Year", - "print_hide": 1, - "reqd": 1, - "search_index": 1 - }, { "fieldname": "column_break3", "fieldtype": "Column Break" @@ -245,7 +234,8 @@ ], "icon": "fa fa-shopping-cart", "is_submittable": 1, - "modified": "2019-09-24 15:08:32.750661", + "links": [], + "modified": "2020-06-25 14:37:21.140194", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", 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 95db33b0f8..4b852300e5 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -25,6 +25,7 @@ class RequestforQuotation(BuyingController): self.validate_duplicate_supplier() self.validate_supplier_list() validate_for_items(self) + super(RequestforQuotation, self).set_qty_as_per_stock_uom() self.update_email_id() def validate_duplicate_supplier(self): @@ -50,7 +51,7 @@ class RequestforQuotation(BuyingController): def validate_email_id(self, args): if not args.email_id: - frappe.throw(_("Row {0}: For supplier {0} Email Address is required to send email").format(args.idx, args.supplier)) + frappe.throw(_("Row {0}: For Supplier {0}, Email Address is Required to Send Email").format(args.idx, args.supplier)) def on_submit(self): frappe.db.set(self, 'status', 'Submitted') @@ -153,7 +154,7 @@ class RequestforQuotation(BuyingController): sender=sender,attachments = attachments, send_email=True, doctype=self.doctype, name=self.name)["name"] - frappe.msgprint(_("Email sent to supplier {0}").format(data.supplier)) + frappe.msgprint(_("Email Sent to Supplier {0}").format(data.supplier)) def get_attachments(self): attachments = [d.name for d in get_attachments(self.doctype, self.name)] @@ -192,7 +193,7 @@ def send_supplier_emails(rfq_name): def check_portal_enabled(reference_doctype): if not frappe.db.get_value('Portal Menu Item', {'reference_doctype': reference_doctype}, 'enabled'): - frappe.throw(_("Request for Quotation is disabled to access from portal, for more check portal settings.")) + 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 @@ -205,6 +206,7 @@ def get_list_context(context=None): }) return list_context +@frappe.whitelist() def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select `tabContact`.name from `tabContact`, `tabDynamic Link` where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s @@ -258,7 +260,7 @@ def create_supplier_quotation(doc): sq_doc.flags.ignore_permissions = True sq_doc.run_method("set_missing_values") sq_doc.save() - frappe.msgprint(_("Supplier Quotation {0} created").format(sq_doc.name)) + frappe.msgprint(_("Supplier Quotation {0} Created").format(sq_doc.name)) return sq_doc.name except Exception: return None @@ -278,6 +280,7 @@ def create_rfq_items(sq_doc, supplier, data): "description": data.description, "qty": data.qty, "rate": data.rate, + "conversion_factor": data.conversion_factor if data.conversion_factor else None, "supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"), "warehouse": data.warehouse or '', "request_for_quotation_item": data.name, 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 dbd9f02278..3de9526c4f 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 @@ -6,12 +6,14 @@ from __future__ import unicode_literals import unittest import frappe -from erpnext.templates.pages.rfq import check_supplier_has_docname_access from frappe.utils import nowdate +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.templates.pages.rfq import check_supplier_has_docname_access +from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation +from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation class TestRequestforQuotation(unittest.TestCase): def test_quote_status(self): - from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation rfq = make_request_for_quotation() self.assertEqual(rfq.get('suppliers')[0].quote_status, 'Pending') @@ -31,7 +33,6 @@ class TestRequestforQuotation(unittest.TestCase): self.assertEqual(rfq.get('suppliers')[1].quote_status, 'No Quote') def test_make_supplier_quotation(self): - from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation rfq = make_request_for_quotation() sq = make_supplier_quotation(rfq.name, rfq.get('suppliers')[0].supplier) @@ -51,15 +52,13 @@ class TestRequestforQuotation(unittest.TestCase): self.assertEqual(sq1.get('items')[0].qty, 5) def test_make_supplier_quotation_with_special_characters(self): - from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation - frappe.delete_doc_if_exists("Supplier", "_Test Supplier '1", force=1) supplier = frappe.new_doc("Supplier") supplier.supplier_name = "_Test Supplier '1" supplier.supplier_group = "_Test Supplier Group" supplier.insert() - rfq = make_request_for_quotation(supplier_wt_appos) + rfq = make_request_for_quotation(supplier_data=supplier_wt_appos) sq = make_supplier_quotation(rfq.name, supplier_wt_appos[0].get("supplier")) sq.submit() @@ -76,7 +75,6 @@ class TestRequestforQuotation(unittest.TestCase): frappe.form_dict.name = None def test_make_supplier_quotation_from_portal(self): - from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation rfq = make_request_for_quotation() rfq.get('items')[0].rate = 100 rfq.supplier = rfq.suppliers[0].supplier @@ -90,12 +88,34 @@ class TestRequestforQuotation(unittest.TestCase): 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 + }) + row.db_update() -def make_request_for_quotation(supplier_data=None): + 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) + + self.assertEqual(supplier_quotation.items[0].qty, 5) + self.assertEqual(supplier_quotation.items[0].stock_qty, 10) + +def make_request_for_quotation(**args): """ :param supplier_data: List containing supplier data """ - supplier_data = supplier_data if supplier_data else get_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.transaction_date = nowdate() rfq.status = 'Draft' @@ -106,11 +126,13 @@ def make_request_for_quotation(supplier_data=None): rfq.append('suppliers', data) rfq.append("items", { - "item_code": "_Test Item", + "item_code": args.item_code or "_Test Item", "description": "_Test Item", - "uom": "_Test UOM", - "qty": 5, - "warehouse": "_Test Warehouse - _TC", + "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() }) diff --git a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json index 0159df962e..408f49f523 100644 --- a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json +++ b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2016-02-25 08:04:02.452958", "doctype": "DocType", @@ -9,6 +10,7 @@ "supplier_part_no", "column_break_3", "item_name", + "schedule_date", "section_break_5", "description", "item_group", @@ -18,9 +20,11 @@ "image_view", "quantity", "qty", + "stock_uom", "col_break2", - "schedule_date", "uom", + "conversion_factor", + "stock_qty", "warehouse_and_reference", "warehouse", "project_name", @@ -33,7 +37,7 @@ "fields": [ { "bold": 1, - "columns": 3, + "columns": 2, "fieldname": "item_code", "fieldtype": "Link", "in_list_view": 1, @@ -98,7 +102,7 @@ { "fieldname": "quantity", "fieldtype": "Section Break", - "label": "Quantity" + "label": "Quantity & Stock" }, { "bold": 1, @@ -129,12 +133,12 @@ { "fieldname": "uom", "fieldtype": "Link", + "in_list_view": 1, "label": "UOM", "oldfieldname": "uom", "oldfieldtype": "Link", "options": "UOM", "print_width": "100px", - "read_only": 1, "reqd": 1, "width": "100px" }, @@ -144,7 +148,7 @@ "label": "Warehouse and Reference" }, { - "columns": 3, + "columns": 2, "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, @@ -202,6 +206,7 @@ }, { "allow_on_submit": 1, + "default": "0", "fieldname": "page_break", "fieldtype": "Check", "label": "Page Break", @@ -219,10 +224,36 @@ { "fieldname": "section_break_23", "fieldtype": "Section Break" + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "UOM Conversion Factor", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "stock_qty", + "fieldtype": "Float", + "label": "Qty as per Stock UOM", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "istable": 1, - "modified": "2019-05-01 17:50:23.703801", + "links": [], + "modified": "2020-06-12 19:10:36.333441", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation Item", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 3bc441af6d..7db1516ce1 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -761,7 +761,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nSubmitted\nStopped\nCancelled", + "options": "\nDraft\nSubmitted\nStopped\nCancelled\nExpired", "print_hide": 1, "read_only": 1, "reqd": 1, @@ -803,7 +803,7 @@ "idx": 29, "is_submittable": 1, "links": [], - "modified": "2020-04-15 11:44:52.958022", + "modified": "2020-05-15 21:24:12.639482", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/buying/module_onboarding/buying/buying.json b/erpnext/buying/module_onboarding/buying/buying.json new file mode 100644 index 0000000000..887f85b82d --- /dev/null +++ b/erpnext/buying/module_onboarding/buying/buying.json @@ -0,0 +1,53 @@ +{ + "allow_roles": [ + { + "role": "Purchase Manager" + }, + { + "role": "Purchase User" + }, + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + } + ], + "creation": "2020-05-06 15:56:35.049205", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/buying", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-08 14:05:28.273641", + "modified_by": "Administrator", + "module": "Buying", + "name": "Buying", + "owner": "Administrator", + "steps": [ + { + "step": "Introduction to Buying" + }, + { + "step": "Create a Supplier" + }, + { + "step": "Setup your Warehouse" + }, + { + "step": "Create a Product" + }, + { + "step": "Create a Material Request" + }, + { + "step": "Create your first Purchase Order" + }, + { + "step": "Buying Settings" + } + ], + "subtitle": "Products, Purchases, Analysis, and more.", + "success_message": "The Buying Module is all set up!", + "title": "Let's Set Up the Buying Module." +} \ No newline at end of file diff --git a/erpnext/buying/onboarding_step/buying_settings/buying_settings.json b/erpnext/buying/onboarding_step/buying_settings/buying_settings.json new file mode 100644 index 0000000000..a788ccd4cc --- /dev/null +++ b/erpnext/buying/onboarding_step/buying_settings/buying_settings.json @@ -0,0 +1,19 @@ +{ + "action": "Update Settings", + "creation": "2020-05-06 15:53:44.667414", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-12 18:30:06.323797", + "modified_by": "Administrator", + "name": "Buying Settings", + "owner": "Administrator", + "reference_document": "Buying Settings", + "show_full_form": 0, + "title": "Configure Buying Settings.", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/buying/onboarding_step/create_a_material_request/create_a_material_request.json b/erpnext/buying/onboarding_step/create_a_material_request/create_a_material_request.json new file mode 100644 index 0000000000..9dc493dd49 --- /dev/null +++ b/erpnext/buying/onboarding_step/create_a_material_request/create_a_material_request.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-15 14:39:09.818764", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-15 14:39:09.818764", + "modified_by": "Administrator", + "name": "Create a Material Request", + "owner": "Administrator", + "reference_document": "Material Request", + "show_full_form": 1, + "title": "Create a Material Request", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/buying/onboarding_step/create_a_product/create_a_product.json b/erpnext/buying/onboarding_step/create_a_product/create_a_product.json new file mode 100644 index 0000000000..d2068e167b --- /dev/null +++ b/erpnext/buying/onboarding_step/create_a_product/create_a_product.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 18:16:06.624554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-12 18:30:02.489949", + "modified_by": "Administrator", + "name": "Create a Product", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create a Product", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/buying/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/buying/onboarding_step/create_a_supplier/create_a_supplier.json new file mode 100644 index 0000000000..7a64224bd4 --- /dev/null +++ b/erpnext/buying/onboarding_step/create_a_supplier/create_a_supplier.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 22:09:10.043554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 22:09:10.043554", + "modified_by": "Administrator", + "name": "Create a Supplier", + "owner": "Administrator", + "reference_document": "Supplier", + "show_full_form": 0, + "title": "Create a Supplier", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/buying/onboarding_step/create_your_first_purchase_order/create_your_first_purchase_order.json b/erpnext/buying/onboarding_step/create_your_first_purchase_order/create_your_first_purchase_order.json new file mode 100644 index 0000000000..9dbed23978 --- /dev/null +++ b/erpnext/buying/onboarding_step/create_your_first_purchase_order/create_your_first_purchase_order.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 18:17:49.976035", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-12 18:31:56.856112", + "modified_by": "Administrator", + "name": "Create your first Purchase Order", + "owner": "Administrator", + "reference_document": "Purchase Order", + "show_full_form": 0, + "title": "Create your first Purchase Order", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/buying/onboarding_step/introduction_to_buying/introduction_to_buying.json b/erpnext/buying/onboarding_step/introduction_to_buying/introduction_to_buying.json new file mode 100644 index 0000000000..fd98fddafa --- /dev/null +++ b/erpnext/buying/onboarding_step/introduction_to_buying/introduction_to_buying.json @@ -0,0 +1,19 @@ +{ + "action": "Watch Video", + "creation": "2020-05-06 15:37:09.477765", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-12 18:25:08.509900", + "modified_by": "Administrator", + "name": "Introduction to Buying", + "owner": "Administrator", + "show_full_form": 0, + "title": "Introduction to Buying", + "validate_action": 1, + "video_url": "https://youtu.be/efFajTTQBa8" +} \ No newline at end of file diff --git a/erpnext/buying/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/buying/onboarding_step/setup_your_warehouse/setup_your_warehouse.json new file mode 100644 index 0000000000..9457deee26 --- /dev/null +++ b/erpnext/buying/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -0,0 +1,20 @@ +{ + "action": "Go to Page", + "creation": "2020-05-19 18:54:19.383397", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-04 12:33:16.970031", + "modified_by": "Administrator", + "name": "Setup your Warehouse", + "owner": "Administrator", + "path": "Tree/Warehouse", + "reference_document": "Warehouse", + "show_full_form": 0, + "title": "Set up your Warehouse", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 39668795cb..88a865f0f8 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.utils import flt def execute(filters=None): columns = get_columns(filters) @@ -54,15 +55,16 @@ def get_columns(filters): "width": 140 }, { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 200 + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 150 }, { "label": _("Quantity"), "fieldname": "quantity", - "fieldtype": "Int", + "fieldtype": "Float", "width": 140 }, { @@ -118,7 +120,7 @@ def get_columns(filters): }, { "label": _("Purchase Order Amount(Company Currency)"), - "fieldname": "purchase_order_amt_usd", + "fieldname": "purchase_order_amt_in_company_currency", "fieldtype": "Float", "width": 140 }, @@ -175,17 +177,17 @@ def get_data(filters): "requesting_site": po.warehouse, "requestor": po.owner, "material_request_no": po.material_request, - "description": po.description, - "quantity": po.qty, + "item_code": po.item_code, + "quantity": flt(po.qty), "unit_of_measurement": po.stock_uom, "status": po.status, "purchase_order_date": po.transaction_date, "purchase_order": po.parent, "supplier": po.supplier, - "estimated_cost": mr_record.get('amount'), - "actual_cost": pi_records.get(po.name), - "purchase_order_amt": po.amount, - "purchase_order_amt_in_company_currency": po.base_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) } @@ -198,9 +200,14 @@ def get_mapped_mr_details(conditions): SELECT par.transaction_date, par.per_ordered, + par.owner, child.name, child.parent, - child.amount + child.amount, + child.qty, + child.item_code, + child.uom, + par.status FROM `tabMaterial Request` par, `tabMaterial Request Item` child WHERE par.per_ordered>=0 @@ -217,7 +224,15 @@ def get_mapped_mr_details(conditions): procurement_record_details = dict( material_request_date=record.transaction_date, material_request_no=record.parent, - estimated_cost=record.amount + requestor=record.owner, + item_code=record.item_code, + estimated_cost=flt(record.amount), + quantity=flt(record.qty), + unit_of_measurement=record.uom, + status=record.status, + actual_cost=0, + purchase_order_amt=0, + purchase_order_amt_in_company_currency=0 ) procurement_record_against_mr.append(procurement_record_details) return mr_records, procurement_record_against_mr @@ -259,7 +274,7 @@ def get_po_entries(conditions): child.warehouse, child.material_request, child.material_request_item, - child.description, + child.item_code, child.stock_uom, child.qty, child.amount, diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py index bebf0ccec5..44ab767c0a 100644 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py @@ -15,7 +15,7 @@ class TestProcurementTracker(unittest.TestCase): def test_result_for_procurement_tracker(self): filters = { 'company': '_Test Procurement Company', - 'cost_center': '_Test Cost Center - _TC' + 'cost_center': 'Main - _TPC' } expected_data = self.generate_expected_data() report = execute(filters) @@ -33,24 +33,27 @@ class TestProcurementTracker(unittest.TestCase): country="Pakistan" )).insert() warehouse = create_warehouse("_Test Procurement Warehouse", company="_Test Procurement Company") - mr = make_material_request(company="_Test Procurement Company", warehouse=warehouse) + 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 = "_Test Cost Center - _TC" + po.get("items")[0].cost_center = "Main - _TPC" po.submit() pr = make_purchase_receipt(po.name) + pr.get("items")[0].cost_center = "Main - _TPC" pr.submit() frappe.db.commit() date_obj = datetime.date(datetime.now()) + po.load_from_db() + expected_data = { "material_request_date": date_obj, - "cost_center": "_Test Cost Center - _TC", + "cost_center": "Main - _TPC", "project": None, "requesting_site": "_Test Procurement Warehouse - _TPC", "requestor": "Administrator", "material_request_no": mr.name, - "description": '_Test Item 1', + "item_code": '_Test Item', "quantity": 10.0, "unit_of_measurement": "_Test UOM", "status": "To Bill", @@ -58,10 +61,11 @@ class TestProcurementTracker(unittest.TestCase): "purchase_order": po.name, "supplier": "_Test Supplier", "estimated_cost": 0.0, - "actual_cost": None, - "purchase_order_amt": 5000.0, - "purchase_order_amt_in_company_currency": 300000.0, + "actual_cost": 0.0, + "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 } + return expected_data \ No newline at end of file diff --git a/erpnext/buying/report/requested_items_to_be_ordered/__init__.py b/erpnext/buying/report/purchase_order_analysis/__init__.py similarity index 100% rename from erpnext/buying/report/requested_items_to_be_ordered/__init__.py rename to erpnext/buying/report/purchase_order_analysis/__init__.py diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js new file mode 100644 index 0000000000..701da4380a --- /dev/null +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js @@ -0,0 +1,78 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Purchase Order Analysis"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "width": "80", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_default("company") + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname": "purchase_order", + "label": __("Purchase Order"), + "fieldtype": "Link", + "width": "80", + "options": "Purchase Order", + "get_query": () =>{ + return { + filters: { "docstatus": 1 } + } + } + }, + { + "fieldname": "status", + "label": __("Status"), + "fieldtype": "MultiSelectList", + "width": "80", + get_data: function(txt) { + let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"] + let options = [] + for (let option of status){ + options.push({ + "value": option, + "description": "" + }) + } + return options + } + }, + { + "fieldname": "group_by_po", + "label": __("Group by Purchase Order"), + "fieldtype": "Check", + "default": 0 + } + ], + + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + let format_fields = ["received_qty", "billed_amount"]; + + if (in_list(format_fields, column.fieldname) && data && data[column.fieldname] > 0) { + value = "" + value + ""; + } + return value; + } +}; diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.json b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.json new file mode 100644 index 0000000000..5ba3101ec5 --- /dev/null +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.json @@ -0,0 +1,33 @@ +{ + "add_total_row": 1, + "creation": "2020-05-04 18:41:28.625119", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-05-15 20:57:52.623455", + "modified_by": "Administrator", + "module": "Buying", + "name": "Purchase Order Analysis", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Order", + "report_name": "Purchase Order Analysis", + "report_type": "Script Report", + "roles": [ + { + "role": "Purchase Manager" + }, + { + "role": "Purchase User" + }, + { + "role": "Stock User" + }, + { + "role": "Supplier" + } + ] +} \ No newline at end of file diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py new file mode 100644 index 0000000000..89be62231b --- /dev/null +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -0,0 +1,271 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import copy +from frappe import _ +from frappe.utils import flt, date_diff, getdate + +def execute(filters=None): + if not filters: + return [], [] + + validate_filters(filters) + + columns = get_columns(filters) + conditions = get_conditions(filters) + + data = get_data(conditions, filters) + + if not data: + return [], [], None, [] + + 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") + + if not from_date and to_date: + frappe.throw(_("From and To Dates are required.")) + 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" + + if filters.get("company"): + conditions += " and po.company = %(company)s" + + if filters.get("purchase_order"): + conditions += " and po.name = %(purchase_order)s" + + if filters.get("status"): + conditions += " and po.status in %(status)s" + + return conditions + +def get_data(conditions, filters): + data = frappe.db.sql(""" + SELECT + po.transaction_date as date, + poi.schedule_date as required_date, + po.name as purchase_order, + po.status, po.supplier, poi.item_code, + poi.qty, poi.received_qty, + (poi.qty - poi.received_qty) AS pending_qty, + IFNULL(pii.qty, 0) as billed_qty, + poi.base_amount as amount, + (poi.received_qty * poi.base_rate) as received_qty_amount, + (poi.billed_amt * IFNULL(po.conversion_rate, 1)) as billed_amount, + (poi.base_amount - (poi.billed_amt * IFNULL(po.conversion_rate, 1))) as pending_amount, + po.set_warehouse as warehouse, + po.company, poi.name + FROM + `tabPurchase Order` po, + `tabPurchase Order Item` poi + LEFT JOIN `tabPurchase Invoice Item` pii + ON pii.po_detail = poi.name + WHERE + poi.parent = po.name + and po.status not in ('Stopped', 'Closed') + and po.docstatus = 1 + {0} + GROUP BY poi.name + ORDER BY po.transaction_date ASC + """.format(conditions), filters, as_dict=1) + + return data + +def prepare_data(data, filters): + completed, pending = 0, 0 + pending_field = "pending_amount" + completed_field = "billed_amount" + + if filters.get("group_by_po"): + purchase_order_map = {} + + for row in data: + # sum data for chart + completed += row[completed_field] + pending += row[pending_field] + + # prepare data for report view + row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"]) + + if filters.get("group_by_po"): + po_name = row["purchase_order"] + + if not po_name in purchase_order_map: + # create an entry + row_copy = copy.deepcopy(row) + purchase_order_map[po_name] = row_copy + else: + # update existing entry + po_row = purchase_order_map[po_name] + 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"] + for field in fields: + po_row[field] = flt(row[field]) + flt(po_row[field]) + + chart_data = prepare_chart_data(pending, completed) + + if filters.get("group_by_po"): + data = [] + for po in purchase_order_map: + data.append(purchase_order_map[po]) + return data, chart_data + + 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 + } + +def get_columns(filters): + columns = [ + { + "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 + }, + { + "label": _("Supplier"), + "fieldname": "supplier", + "fieldtype": "Link", + "options": "Supplier", + "width": 130 + }] + + if not filters.get("group_by_po"): + 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 + } + ]) + + 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 888676cf64..1ed6cad6b4 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +from frappe import _ from erpnext.controllers.trends import get_columns,get_data def execute(filters=None): @@ -10,5 +11,48 @@ def execute(filters=None): data = [] conditions = get_columns(filters, "Purchase Order") data = get_data(filters, conditions) + chart_data = get_chart_data(data, conditions, filters) - return conditions["columns"], data \ No newline at end of file + return conditions["columns"], data, None, chart_data + +def get_chart_data(data, conditions, filters): + if not (data and conditions): + return [] + + datapoints = [] + + start = 2 if filters.get("based_on") in ["Item", "Supplier"] else 1 + if filters.get("group_by"): + start += 1 + + # fetch only periodic columns as labels + columns = conditions.get("columns")[start:-2][1::2] + 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]: + 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] + + 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 + } + ] + }, + "type" : "line", + "lineOptions": { + "regionFill": 1 + } + } \ No newline at end of file diff --git a/erpnext/buying/report/requested_items_to_be_ordered/requested_items_to_be_ordered.json b/erpnext/buying/report/requested_items_to_be_ordered/requested_items_to_be_ordered.json deleted file mode 100644 index bb112698d3..0000000000 --- a/erpnext/buying/report/requested_items_to_be_ordered/requested_items_to_be_ordered.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "add_total_row": 1, - "creation": "2013-05-13 16:10:02", - "disable_prepared_report": 0, - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2019-04-18 19:02:03.099422", - "modified_by": "Administrator", - "module": "Buying", - "name": "Requested Items To Be Ordered", - "owner": "Administrator", - "prepared_report": 0, - "query": "select \n mr.name as \"Material Request:Link/Material Request:120\",\n\tmr.transaction_date as \"Date:Date:100\",\n\tmr_item.item_code as \"Item Code:Link/Item:120\",\n\tsum(ifnull(mr_item.stock_qty, 0)) as \"Qty:Float:100\",\n\tifnull(mr_item.stock_uom, '') as \"UOM:Link/UOM:100\",\n\tsum(ifnull(mr_item.ordered_qty, 0)) as \"Ordered Qty:Float:100\", \n\t(sum(mr_item.stock_qty) - sum(ifnull(mr_item.ordered_qty, 0))) as \"Qty to Order:Float:100\",\n\tmr_item.item_name as \"Item Name::150\",\n\tmr_item.description as \"Description::200\",\n\tmr.company as \"Company:Link/Company:\"\nfrom\n\t`tabMaterial Request` mr, `tabMaterial Request Item` mr_item\nwhere\n\tmr_item.parent = mr.name\n\tand mr.material_request_type = \"Purchase\"\n\tand mr.docstatus = 1\n\tand mr.status != \"Stopped\"\ngroup by mr.name, mr_item.item_code\nhaving\n\tsum(ifnull(mr_item.ordered_qty, 0)) < sum(ifnull(mr_item.stock_qty, 0))\norder by mr.transaction_date asc", - "ref_doctype": "Purchase Order", - "report_name": "Requested Items To Be Ordered", - "report_type": "Query Report", - "roles": [ - { - "role": "Stock User" - }, - { - "role": "Purchase Manager" - }, - { - "role": "Purchase User" - } - ] - } \ No newline at end of file diff --git a/erpnext/hr/doctype/additional_salary/__init__.py b/erpnext/buying/report/requested_items_to_order/__init__.py similarity index 100% rename from erpnext/hr/doctype/additional_salary/__init__.py rename to erpnext/buying/report/requested_items_to_order/__init__.py diff --git a/erpnext/buying/report/requested_items_to_order/requested_items_to_order.js b/erpnext/buying/report/requested_items_to_order/requested_items_to_order.js new file mode 100644 index 0000000000..9555e8252a --- /dev/null +++ b/erpnext/buying/report/requested_items_to_order/requested_items_to_order.js @@ -0,0 +1,76 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Requested Items to Order"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "width": "80", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_default("company") + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname": "material_request", + "label": __("Material Request"), + "fieldtype": "Link", + "width": "80", + "options": "Material Request", + "get_query": () => { + return { + filters: { + "docstatus": 1, + "material_request_type": "Purchase", + "per_received": ["<", 100] + } + } + } + }, + { + "fieldname": "item_code", + "label": __("Item"), + "fieldtype": "Link", + "width": "80", + "options": "Item", + "get_query": () => { + return { + query: "erpnext.controllers.queries.item_query" + } + } + }, + { + "fieldname": "group_by_mr", + "label": __("Group by Material Request"), + "fieldtype": "Check", + "default": 0 + } + ], + + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname == "ordered_qty" && data && data.ordered_qty > 0) { + value = "" + value + ""; + } + return value; + } +}; diff --git a/erpnext/buying/report/requested_items_to_order/requested_items_to_order.json b/erpnext/buying/report/requested_items_to_order/requested_items_to_order.json new file mode 100644 index 0000000000..4a0578be4b --- /dev/null +++ b/erpnext/buying/report/requested_items_to_order/requested_items_to_order.json @@ -0,0 +1,34 @@ +{ + "add_total_row": 1, + "creation": "2020-05-04 20:23:57.750719", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-05-05 13:05:51.723951", + "modified_by": "Administrator", + "module": "Buying", + "name": "Requested Items to Order", + "owner": "Administrator", + "prepared_report": 0, + "query": "", + "ref_doctype": "Material Request", + "report_name": "Requested Items to Order", + "report_type": "Script Report", + "roles": [ + { + "role": "Purchase Manager" + }, + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + }, + { + "role": "Purchase User" + } + ] +} \ No newline at end of file diff --git a/erpnext/buying/report/requested_items_to_order/requested_items_to_order.py b/erpnext/buying/report/requested_items_to_order/requested_items_to_order.py new file mode 100644 index 0000000000..cca01b104a --- /dev/null +++ b/erpnext/buying/report/requested_items_to_order/requested_items_to_order.py @@ -0,0 +1,233 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import copy +from frappe import _ +from frappe.utils import flt, date_diff, getdate + +def execute(filters=None): + if not filters: + return [],[] + + validate_filters(filters) + + columns = get_columns(filters) + conditions = get_conditions(filters) + + #get queried data + data = get_data(filters, conditions) + + #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") + + if not from_date and to_date: + frappe.throw(_("From and To Dates are required.")) + 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 mr.transaction_date between '{0}' and '{1}'".format(filters.get("from_date"),filters.get("to_date")) + + if filters.get("company"): + conditions += " and mr.company = '{0}'".format(filters.get("company")) + + if filters.get("material_request"): + conditions += " and mr.name = '{0}'".format(filters.get("material_request")) + + if filters.get("item_code"): + conditions += " and mr_item.item_code = '{0}'".format(filters.get("item_code")) + + return conditions + +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(mr_item.stock_qty) - sum(ifnull(mr_item.ordered_qty, 0))) as qty_to_order, + mr_item.item_name as item_name, + 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", "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 = {}, {} + + for row in data: + # 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_to_order" : row["qty_to_order"] + } + else: + item_entry = item_qty_map[row["item_code"]] + update_qty_columns(item_entry, row) + + if filters.get("group_by_mr"): + # consolidated material request map for group by filter + if not row["material_request"] in material_request_map: + # create an entry with mr as key + row_copy = copy.deepcopy(row) + material_request_map[row["material_request"]] = row_copy + else: + 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 + update_qty_columns(mr_row, row) + + chart_data = prepare_chart_data(item_qty_map) + + if filters.get("group_by_mr"): + 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 = [], [], [] + + if len(item_data) > 30: + item_data = dict(list(item_data.items())[:30]) + + for row in item_data: + mr_row = item_data[row] + labels.append(row) + qty_to_order.append(mr_row["qty_to_order"]) + ordered_qty.append(mr_row["ordered_qty"]) + + chart_data = { + "data" : { + "labels": labels, + "datasets": [ + { + 'name': _('Qty to Order'), + 'values': qty_to_order + }, + { + 'name': _('Ordered Qty'), + 'values': ordered_qty + } + ] + }, + "type": "bar", + "barOptions": { + "stacked": 1 + }, + } + + return chart_data + +def get_columns(filters): + columns = [ + { + "label": _("Material Request"), + "fieldname": "material_request", + "fieldtype": "Link", + "options": "Material Request", + "width": 150 + }, + { + "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": _("UOM"), + "fieldname": "uom", + "fieldtype": "Data", + "width": 100, + }]) + + columns.extend([ + { + "label": _("Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty" + }, + { + "label": _("Ordered Qty"), + "fieldname": "ordered_qty", + "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/config/buying.py b/erpnext/config/buying.py index 1d4054786e..b06bb76ca8 100644 --- a/erpnext/config/buying.py +++ b/erpnext/config/buying.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals +import frappe from frappe import _ def get_data(): - return [ + config = [ { "label": _("Purchasing"), "icon": "fa fa-star", @@ -166,7 +167,7 @@ def get_data(): { "type": "report", "is_query_report": True, - "name": "Requested Items To Be Ordered", + "name": "Requested Items To Order", "reference_doctype": "Material Request", "onboard": 1, }, @@ -243,3 +244,21 @@ def get_data(): }, ] + + regional = { + "label": _("Regional"), + "items": [ + { + "type": "doctype", + "name": "Import Supplier Invoice", + "description": _("Import Italian Supplier Invoice."), + "onboard": 1, + } + ] + } + + countries = frappe.get_all("Company", fields="country") + countries = [country["country"] for country in countries] + if "Italy" in countries: + config.append(regional) + return config \ No newline at end of file diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index 2c18eeb83a..012f1cad0a 100644 --- a/erpnext/config/manufacturing.py +++ b/erpnext/config/manufacturing.py @@ -120,13 +120,7 @@ def get_data(): { "type": "report", "is_query_report": True, - "name": "Open Work Orders", - "doctype": "Work Order" - }, - { - "type": "report", - "is_query_report": True, - "name": "Work Orders in Progress", + "name": "Work Order Summary", "doctype": "Work Order" }, { @@ -135,12 +129,6 @@ def get_data(): "name": "Issued Items Against Work Order", "doctype": "Work Order" }, - { - "type": "report", - "is_query_report": True, - "name": "Completed Work Orders", - "doctype": "Work Order" - }, { "type": "report", "is_query_report": True, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index eecb143d55..ead503ed09 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -20,6 +20,7 @@ from erpnext.exceptions import InvalidCurrency from six import text_type from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions from erpnext.stock.get_item_details import get_item_warehouse +from erpnext.stock.doctype.packed_item.packed_item import make_packing_list force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") @@ -1137,8 +1138,8 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, child_item.item_name = item.item_name child_item.description = item.description child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date + child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.uom = item.stock_uom - child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 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.") @@ -1157,8 +1158,8 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.item_name = item.item_name child_item.description = item.description child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date + child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.uom = item.stock_uom - child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation return child_item @@ -1190,6 +1191,26 @@ def check_and_delete_children(parent, data): @frappe.whitelist() def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): + def check_permissions(doc, perm_type='create'): + try: + doc.check_permission(perm_type) + except: + action = "add" if perm_type == 'create' else "update" + frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions")) + + def get_new_child_item(item_row): + if parent_doctype == "Sales Order": + return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) + if parent_doctype == "Purchase Order": + return set_purchase_order_defaults(parent_doctype, parent_doctype_name, 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): + frappe.throw(_("Cannot set quantity less than delivered quantity")) + + if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty): + frappe.throw(_("Cannot set quantity less than received quantity")) + data = json.loads(trans_items) sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] @@ -1201,20 +1222,29 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil new_child_flag = False if not d.get("docname"): new_child_flag = True - if parent_doctype == "Sales Order": - child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d) - if parent_doctype == "Purchase Order": - child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d) + check_permissions(parent, 'create') + child_item = get_new_child_item(d) else: + check_permissions(parent, 'write') child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) - if flt(child_item.get("rate")) == flt(d.get("rate")) and flt(child_item.get("qty")) == flt(d.get("qty")): + + 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")) + + if parent_doctype == 'Sales Order': + prev_date, new_date = child_item.get("delivery_date"), d.get("delivery_date") + 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 + conversion_factor_unchanged = prev_con_fac == new_con_fac + date_unchanged = prev_date == 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 date_unchanged: continue - if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): - frappe.throw(_("Cannot set quantity less than delivered quantity")) - - if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty): - frappe.throw(_("Cannot set quantity less than received quantity")) + validate_quantity(child_item, d) child_item.qty = flt(d.get("qty")) precision = child_item.precision("rate") or 2 @@ -1225,6 +1255,18 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil else: child_item.rate = flt(d.get("rate")) + if d.get("conversion_factor"): + if child_item.stock_uom == child_item.uom: + child_item.conversion_factor = 1 + else: + child_item.conversion_factor = flt(d.get('conversion_factor')) + + 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 flt(child_item.price_list_rate): if flt(child_item.rate) > flt(child_item.price_list_rate): # if rate is greater than price_list_rate, set margin @@ -1260,6 +1302,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.set_qty_as_per_stock_uom() parent.calculate_taxes_and_totals() 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) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 608e537e1b..89b48f07ee 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -349,7 +349,7 @@ class BuyingController(StockController): }) if not rm.rate: - rm.rate = get_valuation_rate(raw_material_data.item_code, self.supplier_warehouse, + rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse, self.doctype, self.name, currency=self.company_currency, company=self.company) rm.amount = qty * flt(rm.rate) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index 29f8dd5702..1f95e00424 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -70,7 +70,7 @@ def validate_item_variant_attributes(item, args=None): else: attributes_list = attribute_values.get(attribute.lower(), []) - validate_item_attribute_value(attributes_list, attribute, value, item.name) + 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 @@ -93,13 +93,20 @@ def validate_is_incremental(numeric_attribute, attribute, value, item): .format(attribute, from_range, to_range, increment, item), InvalidItemAttributeValueError, title=_('Invalid Attribute')) -def validate_item_attribute_value(attributes_list, attribute, attribute_value, item): +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: - frappe.throw(_("The value {0} is already assigned to an exisiting Item {2}.").format( - attribute_value, attribute, item), InvalidItemAttributeValueError, title=_('Rename Not Allowed')) + 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")) + 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.throw(msg, InvalidItemAttributeValueError, title=_('Edit Not Allowed')) def get_attribute_values(item): if not frappe.flags.attribute_values: diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 5febfd6bf2..31e34987be 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -8,11 +8,15 @@ from frappe.desk.reportview import get_match_cond, get_filters_cond from frappe.utils import nowdate, getdate from collections import defaultdict from erpnext.stock.get_item_details import _get_item_tax_template +from frappe.utils import unique - # searches for active employees +# searches for active employees +@frappe.whitelist() def employee_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] - return frappe.db.sql("""select name, employee_name from `tabEmployee` + fields = get_fields("Employee", ["name", "employee_name"]) + + return frappe.db.sql("""select {fields} from `tabEmployee` where status = 'Active' and docstatus < 2 and ({key} like %(txt)s @@ -24,6 +28,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): 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) @@ -34,9 +39,13 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) - # searches for leads which are not converted + +# searches for leads which are not converted +@frappe.whitelist() def lead_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select name, lead_name, company_name from `tabLead` + fields = get_fields("Lead", ["name", "lead_name", "company_name"]) + + return frappe.db.sql("""select {fields} from `tabLead` where docstatus < 2 and ifnull(status, '') != 'Converted' and ({key} like %(txt)s @@ -50,6 +59,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, lead_name limit %(start)s, %(page_len)s""".format(**{ + 'fields': ", ".join(fields), 'key': searchfield, 'mcond':get_match_cond(doctype) }), { @@ -59,7 +69,9 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) + # searches for customer +@frappe.whitelist() def customer_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] cust_master_name = frappe.defaults.get_user_default("cust_master_name") @@ -69,13 +81,9 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): else: fields = ["name", "customer_name", "customer_group", "territory"] - meta = frappe.get_meta("Customer") - searchfields = meta.get_search_fields() - searchfields = searchfields + [f for f in [searchfield or "name", "customer_name"] \ - if not f in searchfields] - fields = fields + [f for f in searchfields if not f in fields] + fields = get_fields("Customer", fields) - fields = ", ".join(fields) + 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` @@ -88,7 +96,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, customer_name limit %(start)s, %(page_len)s""".format(**{ - "fields": fields, + "fields": ", ".join(fields), "scond": searchfields, "mcond": get_match_cond(doctype), "fcond": get_filters_cond(doctype, filters, conditions).replace('%', '%%'), @@ -99,14 +107,17 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) + # searches for supplier +@frappe.whitelist() def supplier_query(doctype, txt, searchfield, start, page_len, filters): supp_master_name = frappe.defaults.get_user_default("supp_master_name") if supp_master_name == "Supplier Name": fields = ["name", "supplier_group"] else: fields = ["name", "supplier_name", "supplier_group"] - fields = ", ".join(fields) + + fields = get_fields("Supplier", fields) return frappe.db.sql("""select {field} from `tabSupplier` where docstatus < 2 @@ -119,7 +130,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): idx desc, name, supplier_name limit %(start)s, %(page_len)s """.format(**{ - 'field': fields, + 'field': ', '.join(fields), 'key': searchfield, 'mcond':get_match_cond(doctype) }), { @@ -129,6 +140,8 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len }) + +@frappe.whitelist() def tax_account_query(doctype, txt, searchfield, start, page_len, filters): company_currency = erpnext.get_company_currency(filters.get('company')) @@ -153,6 +166,8 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): return tax_accounts + +@frappe.whitelist() def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): conditions = [] @@ -179,12 +194,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals # scan description only if items are less than 50000 description_cond = 'or tabItem.description LIKE %(txt)s' - extra_cond = " and tabItem.has_variants=0" - if (filters and isinstance(filters, dict) - and filters.get("doctype") == "BOM"): - extra_cond = "" - del filters["doctype"] - return frappe.db.sql("""select tabItem.name, if(length(tabItem.item_name) > 40, concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name, @@ -195,10 +204,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals from tabItem where tabItem.docstatus < 2 and tabItem.disabled=0 + and tabItem.has_variants=0 and (tabItem.end_of_life > %(today)s or ifnull(tabItem.end_of_life, '0000-00-00')='0000-00-00') and ({scond} or tabItem.item_code IN (select parent from `tabItem Barcode` where barcode LIKE %(txt)s) {description_cond}) - {extra_cond} {fcond} {mcond} order by if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), @@ -209,7 +218,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals key=searchfield, columns=columns, scond=searchfields, - extra_cond=extra_cond, fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), mcond=get_match_cond(doctype).replace('%', '%%'), description_cond = description_cond), @@ -221,10 +229,13 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals "page_len": page_len }, as_dict=as_dict) + +@frappe.whitelist() def bom(doctype, txt, searchfield, start, page_len, filters): conditions = [] + fields = get_fields("BOM", ["name", "item"]) - return frappe.db.sql("""select tabBOM.name, tabBOM.item + return frappe.db.sql("""select {fields} from tabBOM where tabBOM.docstatus=1 and tabBOM.is_active=1 @@ -234,6 +245,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), 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), @@ -244,13 +256,17 @@ def bom(doctype, txt, searchfield, start, page_len, filters): 'page_len': page_len or 20 }) + +@frappe.whitelist() def get_project_name(doctype, txt, searchfield, start, page_len, filters): cond = '' if filters.get('customer'): cond = """(`tabProject`.customer = %s or ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer"))) - return frappe.db.sql("""select `tabProject`.name from `tabProject` + fields = get_fields("Project", ["name"]) + + return frappe.db.sql("""select {fields} from `tabProject` where `tabProject`.status not in ("Completed", "Cancelled") and {cond} `tabProject`.name like %(txt)s {match_cond} order by @@ -258,6 +274,7 @@ 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]), cond=cond, match_cond=get_match_cond(doctype), start=start, @@ -267,9 +284,12 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): }) +@frappe.whitelist() 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(""" - select `tabDelivery Note`.name, `tabDelivery Note`.customer, `tabDelivery Note`.posting_date + select %(fields)s from `tabDelivery Note` where `tabDelivery Note`.`%(key)s` like %(txt)s and `tabDelivery Note`.docstatus = 1 @@ -284,6 +304,7 @@ 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), @@ -293,6 +314,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, }, {"txt": ("%%%s%%" % txt)}, as_dict=as_dict) +@frappe.whitelist() def get_batch_no(doctype, txt, searchfield, start, page_len, filters): cond = "" if filters.get("posting_date"): @@ -349,6 +371,8 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): order by expiry_date, name desc limit %(start)s, %(page_len)s""".format(cond, match_conditions=get_match_cond(doctype)), args) + +@frappe.whitelist() def get_account_list(doctype, txt, searchfield, start, page_len, filters): filter_list = [] @@ -371,6 +395,8 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): fields = ["name", "parent_account"], limit_start=start, limit_page_length=page_len, as_list=True) + +@frappe.whitelist() 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 from `tabBlanket Order` bo, `tabBlanket Order Item` boi @@ -385,6 +411,7 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): company = frappe.db.escape(filters.get("company")) )) + @frappe.whitelist() def get_income_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond @@ -490,6 +517,7 @@ def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) + @frappe.whitelist() def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters): item_filters = [ @@ -507,6 +535,7 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters) ) return item_manufacturers + @frappe.whitelist() def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): query = """ @@ -520,6 +549,7 @@ def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) + @frappe.whitelist() def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): query = """ @@ -533,6 +563,7 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(query, filters) + @frappe.whitelist() def get_tax_template(doctype, txt, searchfield, start, page_len, filters): @@ -551,8 +582,19 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): args = { 'item_code': filters.get('item_code'), 'posting_date': filters.get('valid_from'), - 'tax_category': filters.get('tax_category') + 'tax_category': filters.get('tax_category'), + 'company': filters.get('company') } taxes = _get_item_tax_template(args, taxes, for_validate=True) return [(d,) for d in set(taxes)] + + +def get_fields(doctype, fields=[]): + meta = frappe.get_meta(doctype) + fields.extend(meta.get_search_fields()) + + if meta.title_field and not meta.title_field.strip() in fields: + fields.insert(1, meta.title_field.strip()) + + return unique(fields) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 1e0a48c134..b696ac39f6 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -361,7 +361,7 @@ class SellingController(StockController): self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no]))) def set_gross_profit(self): - if self.doctype == "Sales Order": + 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)) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 86de80815d..e8483da544 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -19,7 +19,8 @@ class QualityInspectionNotSubmittedError(frappe.ValidationError): pass class StockController(AccountsController): def validate(self): super(StockController, self).validate() - self.validate_inspection() + if not self.get('is_return'): + self.validate_inspection() self.validate_serialized_batch() self.validate_customer_provided_item() @@ -95,6 +96,7 @@ class StockController(AccountsController): "account": warehouse_account[sle.warehouse]["account"], "against": item_row.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", @@ -105,6 +107,7 @@ class StockController(AccountsController): "account": item_row.expense_account, "against": warehouse_account[sle.warehouse]["account"], "cost_center": item_row.cost_center, + "project": item_row.project or self.get('project'), "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"), @@ -226,7 +229,9 @@ class StockController(AccountsController): def check_expense_account(self, item): if not item.get("expense_account"): - frappe.throw(_("Expense or Difference account is mandatory for Item {0} as it impacts overall stock value").format(item.item_code)) + frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \ + Account in the Items table").format(item.idx, frappe.bold(item.item_code)), + title=_("Expense Account Missing")) else: is_expense_account = frappe.db.get_value("Account", diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 4e568e2795..a9eb9963bf 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -53,7 +53,8 @@ class calculate_taxes_and_totals(object): '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') + 'transaction_date': self.doc.get('transaction_date'), + 'company': self.doc.get('company') } item_group = item_doc.item_group diff --git a/erpnext/controllers/tests/test_mapper.py b/erpnext/controllers/tests/test_mapper.py index 8839e002a4..66459fdbf8 100644 --- a/erpnext/controllers/tests/test_mapper.py +++ b/erpnext/controllers/tests/test_mapper.py @@ -13,14 +13,12 @@ class TestMapper(unittest.TestCase): '''Test mapping of multiple source docs on a single target doc''' make_test_records("Item") - items = frappe.get_all("Item", fields = ["name", "item_code"], filters = {'is_sales_item': 1, 'has_variants': 0, 'disabled': 0}) - customers = frappe.get_all("Customer") - if items and customers: - # Make source docs (quotations) and a target doc (sales order) - customer = random.choice(customers).name - qtn1, item_list_1 = self.make_quotation(items, customer) - qtn2, item_list_2 = self.make_quotation(items, customer) - so, item_list_3 = self.make_sales_order() + 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') + so, item_list_3 = self.make_sales_order() # Map source docs to target with corresponding mapper method method = "erpnext.selling.doctype.quotation.quotation.make_sales_order" @@ -28,18 +26,12 @@ 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.item_code for d in src_items]), + self.assertEqual(set([d for d in src_items]), set([d.item_code for d in updated_so.items])) - def get_random_items(self, items, limit): - '''Get a number of random items from a list of given items''' - random_items = [] - for i in range(0, limit): - random_items.append(random.choice(items)) - return random_items - def make_quotation(self, items, customer): - item_list = self.get_random_items(items, 3) + def make_quotation(self, item_list, customer): + qtn = frappe.get_doc({ "doctype": "Quotation", "quotation_to": "Customer", @@ -49,7 +41,7 @@ class TestMapper(unittest.TestCase): "valid_till" : add_months(nowdate(), 1) }) for item in item_list: - qtn.append("items", {"qty": "2", "item_code": item.item_code}) + qtn.append("items", {"qty": "2", "item_code": item}) qtn.submit() return qtn, item_list @@ -60,7 +52,7 @@ class TestMapper(unittest.TestCase): "base_rate": 100.0, "description": "CPU", "doctype": "Sales Order Item", - "item_code": "_Test Item Home Desktop 100", + "item_code": "_Test Item", "item_name": "CPU", "parentfield": "items", "qty": 10.0, @@ -72,4 +64,4 @@ class TestMapper(unittest.TestCase): }) so = frappe.get_doc(frappe.get_test_records('Sales Order')[0]) so.insert(ignore_permissions=True) - return so, [item] + 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 fd9936bae9..aaeac5d939 100644 --- a/erpnext/controllers/tests/test_qty_based_taxes.py +++ b/erpnext/controllers/tests/test_qty_based_taxes.py @@ -30,6 +30,7 @@ class TestTaxes(unittest.TestCase): self.item_tax_template = frappe.get_doc({ 'doctype': 'Item Tax Template', 'title': uuid4(), + 'company': self.company.name, 'taxes': [ { 'tax_type': self.account.name, diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 092baa4018..9b4b0eb917 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -33,7 +33,7 @@ def validate_filters(filters): frappe.throw(_("{0} is mandatory").format(f)) if not frappe.db.exists("Fiscal Year", filters.get("fiscal_year")): - frappe.throw(_("Fiscal Year: {0} does not exists").format(filters.get("fiscal_year"))) + frappe.throw(_("Fiscal Year {0} Does Not Exist").format(filters.get("fiscal_year"))) if filters.get("based_on") == filters.get("group_by"): frappe.throw(_("'Based On' and 'Group By' can not be same")) diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index ed379389d7..ecf041efd1 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -155,7 +155,7 @@ def has_website_permission(doc, ptype, user, verbose=False): 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, filters={ + return frappe.db.exists(doctype, { 'name': doc.name, fieldname: ["in", suppliers] }) diff --git a/erpnext/crm/dashboard_fixtures.py b/erpnext/crm/dashboard_fixtures.py new file mode 100644 index 0000000000..901c0581f4 --- /dev/null +++ b/erpnext/crm/dashboard_fixtures.py @@ -0,0 +1,221 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe, erpnext, json +from frappe import _ + +def get_data(): + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(), + "number_cards": get_number_cards() + }) + +def get_dashboards(): + return [{ + "doctype": "Dashboard", + "name": "CRM", + "dashboard_name": "CRM", + "charts": [ + { "chart": "Incoming Leads", "width": "Full" }, + { "chart": "Opportunity Trends", "width": "Full"}, + { "chart": "Won Opportunities", "width": "Full" }, + { "chart": "Territory Wise Opportunity Count", "width": "Half"}, + { "chart": "Opportunities via Campaigns", "width": "Half" }, + { "chart": "Territory Wise Sales", "width": "Full"}, + { "chart": "Lead Source", "width": "Half"} + ], + "cards": [ + { "card": "New Lead (Last 1 Month)" }, + { "card": "New Opportunity (Last 1 Month)" }, + { "card": "Won Opportunity (Last 1 Month)" }, + { "card": "Open Opportunity"}, + ] + }] + +def get_company_for_dashboards(): + company = frappe.defaults.get_defaults().company + if company: + return company + else: + company_list = frappe.get_list("Company") + if company_list: + return company_list[0].name + return None + +def get_charts(): + company = get_company_for_dashboards() + + return [{ + "name": "Incoming Leads", + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Count", + "chart_name": _("Incoming Leads"), + "timespan": "Last Quarter", + "time_interval": "Weekly", + "document_type": "Lead", + "based_on": "creation", + 'is_public': 1, + 'timeseries': 1, + "owner": "Administrator", + "filters_json": json.dumps([]), + "type": "Bar" + }, + { + "name": "Opportunity Trends", + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Count", + "chart_name": _("Opportunity Trends"), + "timespan": "Last Quarter", + "time_interval": "Weekly", + "document_type": "Opportunity", + "based_on": "creation", + 'is_public': 1, + 'timeseries': 1, + "owner": "Administrator", + "filters_json": json.dumps([["Opportunity", "company", "=", company, False]]), + "type": "Bar" + }, + { + "name": "Opportunities via Campaigns", + "chart_name": _("Opportunities via Campaigns"), + "doctype": "Dashboard Chart", + "chart_type": "Group By", + "group_by_type": "Count", + "group_by_based_on": "campaign", + "document_type": "Opportunity", + 'is_public': 1, + 'timeseries': 1, + "owner": "Administrator", + "filters_json": json.dumps([["Opportunity", "company", "=", company, False]]), + "type": "Pie", + "custom_options": json.dumps({ + "truncateLegends": 1, + "maxSlices": 8 + }) + }, + { + "name": "Won Opportunities", + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Count", + "chart_name": _("Won Opportunities"), + "timespan": "Last Year", + "time_interval": "Monthly", + "document_type": "Opportunity", + "based_on": "modified", + 'is_public': 1, + 'timeseries': 1, + "owner": "Administrator", + "filters_json": json.dumps([ + ["Opportunity", "company", "=", company, False], + ["Opportunity", "status", "=", "Converted", False]]), + "type": "Bar" + }, + { + "name": "Territory Wise Opportunity Count", + "doctype": "Dashboard Chart", + "chart_type": "Group By", + "group_by_type": "Count", + "group_by_based_on": "territory", + "chart_name": _("Territory Wise Opportunity Count"), + "document_type": "Opportunity", + 'is_public': 1, + "filters_json": json.dumps([ + ["Opportunity", "company", "=", company, False] + ]), + "owner": "Administrator", + "type": "Donut", + "custom_options": json.dumps({ + "truncateLegends": 1, + "maxSlices": 8 + }) + }, + { + "name": "Territory Wise Sales", + "doctype": "Dashboard Chart", + "chart_type": "Group By", + "group_by_type": "Sum", + "group_by_based_on": "territory", + "chart_name": _("Territory Wise Sales"), + "aggregate_function_based_on": "opportunity_amount", + "document_type": "Opportunity", + 'is_public': 1, + "owner": "Administrator", + "filters_json": json.dumps([ + ["Opportunity", "company", "=", company, False], + ["Opportunity", "status", "=", "Converted", False] + ]), + "type": "Bar" + }, + { + "name": "Lead Source", + "doctype": "Dashboard Chart", + "chart_type": "Group By", + "group_by_type": "Count", + "group_by_based_on": "source", + "chart_name": _("Lead Source"), + "document_type": "Lead", + 'is_public': 1, + "owner": "Administrator", + "type": "Pie", + "custom_options": json.dumps({ + "truncateLegends": 1, + "maxSlices": 8 + }) + }] + +def get_number_cards(): + return [{ + "doctype": "Number Card", + "document_type": "Lead", + "name": "New Lead (Last 1 Month)", + "filters_json": json.dumps([ + ["Lead", "creation", "Timespan", "last month"] + ]), + "function": "Count", + "is_public": 1, + "label": _("New Lead (Last 1 Month)"), + "show_percentage_stats": 1, + "stats_time_interval": "Daily" + }, + { + "doctype": "Number Card", + "document_type": "Opportunity", + "name": "New Opportunity (Last 1 Month)", + "filters_json": json.dumps([ + ["Opportunity", "creation", "Timespan", "last month"] + ]), + "function": "Count", + "is_public": 1, + "label": _("New Opportunity (Last 1 Month)"), + "show_percentage_stats": 1, + "stats_time_interval": "Daily" + }, + { + "doctype": "Number Card", + "document_type": "Opportunity", + "name": "Won Opportunity (Last 1 Month)", + "filters_json": json.dumps([ + ["Opportunity", "status", "=", "Converted",False], + ["Opportunity", "creation", "Timespan", "last month"] + ]), + "function": "Count", + "is_public": 1, + "label": _("Won Opportunity (Last 1 Month)"), + "show_percentage_stats": 1, + "stats_time_interval": "Daily" + }, + { + "doctype": "Number Card", + "document_type": "Opportunity", + "name": "Open Opportunity", + "filters_json": json.dumps([["Opportunity","status","=","Open",False]]), + "function": "Count", + "is_public": 1, + "label": _("Open Opportunity"), + "show_percentage_stats": 1, + "stats_time_interval": "Daily" + }] \ No newline at end of file diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json index ca13d6abb6..eb69dc06b6 100644 --- a/erpnext/crm/desk_page/crm/crm.json +++ b/erpnext/crm/desk_page/crm/crm.json @@ -27,26 +27,32 @@ } ], "category": "Modules", - "charts": [], + "charts": [ + { + "chart_name": "Territory Wise Sales" + } + ], "creation": "2020-01-23 14:48:30.183272", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "CRM", - "modified": "2020-04-27 22:32:26.682911", + "modified": "2020-05-28 13:33:52.906750", "modified_by": "Administrator", "module": "CRM", "name": "CRM", + "onboarding": "CRM", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "shortcuts": [ { + "color": "#ffe8cd", "format": "{} Open", "label": "Lead", "link_to": "Lead", @@ -54,6 +60,7 @@ "type": "DocType" }, { + "color": "#cef6d1", "format": "{} Assigned", "label": "Opportunity", "link_to": "Opportunity", @@ -69,6 +76,11 @@ "label": "Sales Analytics", "link_to": "Sales Analytics", "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "CRM", + "type": "Dashboard" } ] } \ No newline at end of file diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index e04ad30ea7..0026e4a02e 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -29,6 +29,9 @@ "requires_fulfilment", "fulfilment_deadline", "fulfilment_terms", + "authorised_by_section", + "signee_company", + "signed_by_company", "sb_references", "document_type", "cb_links", @@ -223,10 +226,28 @@ "options": "Contract", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "signee_company", + "fieldtype": "Signature", + "label": "Signee (Company)" + }, + { + "fieldname": "signed_by_company", + "fieldtype": "Link", + "label": "Signed By (Company)", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "authorised_by_section", + "fieldtype": "Section Break", + "label": "Authorised By" } ], "is_submittable": 1, - "modified": "2019-09-30 00:56:41.559681", + "links": [], + "modified": "2020-03-30 06:56:07.257932", "modified_by": "Administrator", "module": "CRM", "name": "Contract", diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index 18444cc7e6..c39397bf4b 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -29,6 +29,9 @@ class Contract(Document): self.update_contract_status() self.update_fulfilment_status() + def before_submit(self): + self.signed_by_company = frappe.session.user + def before_update_after_submit(self): self.update_contract_status() self.update_fulfilment_status() diff --git a/erpnext/crm/doctype/contract_template/contract_template.json b/erpnext/crm/doctype/contract_template/contract_template.json index b883ce20c9..ef9974f863 100644 --- a/erpnext/crm/doctype/contract_template/contract_template.json +++ b/erpnext/crm/doctype/contract_template/contract_template.json @@ -1,306 +1,106 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:title", - "beta": 0, - "creation": "2018-04-16 06:44:48.791312", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "autoname": "field:title", + "creation": "2018-04-16 06:44:48.791312", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "contract_terms", + "sb_fulfilment", + "requires_fulfilment", + "fulfilment_terms" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sb_terms", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "contract_terms", + "fieldtype": "Text Editor", + "label": "Contract Terms and Conditions", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contract_terms", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contract Terms and Conditions", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "sb_fulfilment", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sb_fulfilment", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "requires_fulfilment", + "fieldtype": "Check", + "label": "Requires Fulfilment" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "requires_fulfilment", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Requires Fulfilment", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.requires_fulfilment==1", - "fieldname": "fulfilment_terms", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Fulfilment Terms and Conditions", - "length": 0, - "no_copy": 0, - "options": "Contract Template Fulfilment Terms", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "depends_on": "eval:doc.requires_fulfilment==1", + "fieldname": "fulfilment_terms", + "fieldtype": "Table", + "label": "Fulfilment Terms and Conditions", + "options": "Contract Template Fulfilment Terms" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-04-17 07:36:05.217599", - "modified_by": "Administrator", - "module": "CRM", - "name": "Contract Template", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-06-03 00:24:58.179816", + "modified_by": "Administrator", + "module": "CRM", + "name": "Contract Template", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 6fef0c4643..f5f8b4efb3 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -6,6 +6,7 @@ "creation": "2013-04-10 11:45:37", "doctype": "DocType", "document_type": "Document", + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "organization_lead", @@ -448,7 +449,7 @@ "idx": 5, "image_field": "image", "links": [], - "modified": "2020-05-11 20:27:45.868960", + "modified": "2020-06-18 14:39:41.835416", "modified_by": "Administrator", "module": "CRM", "name": "Lead", @@ -508,8 +509,10 @@ } ], "search_fields": "lead_name,lead_owner,status", + "sender_field": "email_id", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "subject_field": "title", "title_field": "title" } \ No newline at end of file diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index ec7d14d6ae..315d298eb4 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -114,10 +114,12 @@ class Lead(SellingController): def set_lead_name(self): if not self.lead_name: # Check for leads being created through data import - if not self.company_name and not self.flags.ignore_mandatory: + if not self.company_name and not self.email_id and not self.flags.ignore_mandatory: frappe.throw(_("A Lead requires either a person's name or an organization's name")) - - self.lead_name = self.company_name + elif self.company_name: + self.lead_name = self.company_name + else: + self.lead_name = self.email_id.split("@")[0] def set_title(self): if self.organization_lead: diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 0c9ba495c7..f1b8171349 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -96,6 +96,7 @@ frappe.ui.form.on("Opportunity", { }); } else { frm.add_custom_button(__("Reopen"), function() { + frm.set_value("lost_reasons",[]) frm.set_value("status", "Open"); frm.save(); }); diff --git a/erpnext/crm/doctype/opportunity/opportunity_dashboard.py b/erpnext/crm/doctype/opportunity/opportunity_dashboard.py index 9ed616afd2..68f0104fd6 100644 --- a/erpnext/crm/doctype/opportunity/opportunity_dashboard.py +++ b/erpnext/crm/doctype/opportunity/opportunity_dashboard.py @@ -3,11 +3,7 @@ from frappe import _ def get_data(): return { - 'fieldname': 'prevdoc_docname', - 'non_standard_fieldnames': { - 'Supplier Quotation': 'opportunity', - 'Quotation': 'opportunity' - }, + 'fieldname': 'opportunity', 'transactions': [ { 'items': ['Quotation', 'Supplier Quotation'] diff --git a/erpnext/crm/doctype/sales_stage/sales_stage.json b/erpnext/crm/doctype/sales_stage/sales_stage.json index 4374bb5831..77aa559b77 100644 --- a/erpnext/crm/doctype/sales_stage/sales_stage.json +++ b/erpnext/crm/doctype/sales_stage/sales_stage.json @@ -1,96 +1,44 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:stage_name", - "beta": 0, - "creation": "2018-10-01 09:28:16.399518", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "autoname": "field:stage_name", + "creation": "2018-10-01 09:28:16.399518", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "stage_name" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stage_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Stage Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "stage_name", + "fieldtype": "Data", + "label": "Stage Name", "unique": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-10-01 09:29:43.230378", - "modified_by": "Administrator", - "module": "CRM", - "name": "Sales Stage", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-05-20 12:22:01.866472", + "modified_by": "Administrator", + "module": "CRM", + "name": "Sales Stage", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, "write": 1 } - ], - "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 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json index 2601c14b4d..0a00dca280 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.json +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -30,24 +30,32 @@ "fieldname": "text", "fieldtype": "Small Text", "label": "Tweet", - "mandatory_depends_on": "eval:doc.twitter ==1" + "mandatory_depends_on": "eval:doc.twitter ==1", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "image", "fieldtype": "Attach Image", - "label": "Image" + "label": "Image", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "twitter", "fieldtype": "Check", - "label": "Twitter" + "label": "Twitter", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "linkedin", "fieldtype": "Check", - "label": "LinkedIn" + "label": "LinkedIn", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -56,13 +64,17 @@ "no_copy": 1, "options": "Social Media Post", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.twitter ==1", "fieldname": "content", "fieldtype": "Section Break", - "label": "Twitter" + "label": "Twitter", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -70,7 +82,9 @@ "fieldtype": "Select", "label": "Post Status", "options": "\nScheduled\nPosted\nError", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -78,7 +92,9 @@ "fieldtype": "Data", "hidden": 1, "label": "Twitter Post Id", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -86,68 +102,89 @@ "fieldtype": "Data", "hidden": 1, "label": "LinkedIn Post Id", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "campaign_name", "fieldtype": "Link", "in_list_view": 1, "label": "Campaign", - "options": "Campaign" + "options": "Campaign", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_6", "fieldtype": "Column Break", - "label": "Share On" + "label": "Share On", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_14", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "tweet_preview", - "fieldtype": "HTML" + "fieldtype": "HTML", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "depends_on": "eval:doc.linkedin==1", "fieldname": "linkedin_section", "fieldtype": "Section Break", - "label": "LinkedIn" + "label": "LinkedIn", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "attachments_section", "fieldtype": "Section Break", - "label": "Attachments" + "label": "Attachments", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "linkedin_post", "fieldtype": "Text", "label": "Post", - "mandatory_depends_on": "eval:doc.linkedin ==1" + "mandatory_depends_on": "eval:doc.linkedin ==1", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_15", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "scheduled_time", "fieldtype": "Datetime", "label": "Scheduled Time", - "read_only_depends_on": "eval:doc.post_status == \"Posted\"" + "read_only_depends_on": "eval:doc.post_status == \"Posted\"", + "show_days": 1, + "show_seconds": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-04-21 15:10:04.953713", + "modified": "2020-06-14 10:31:33.961381", "modified_by": "Administrator", "module": "CRM", "name": "Social Media Post", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -157,6 +194,35 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "submit": 1, "write": 1 } ], diff --git a/erpnext/crm/module_onboarding/crm/crm.json b/erpnext/crm/module_onboarding/crm/crm.json new file mode 100644 index 0000000000..8315218c84 --- /dev/null +++ b/erpnext/crm/module_onboarding/crm/crm.json @@ -0,0 +1,41 @@ +{ + "allow_roles": [ + { + "role": "Sales Master Manager" + }, + { + "role": "Sales Manager" + }, + { + "role": "Sales User" + } + ], + "creation": "2020-05-09 23:42:50.901548", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/CRM", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-08 14:05:42.644448", + "modified_by": "Administrator", + "module": "CRM", + "name": "CRM", + "owner": "Administrator", + "steps": [ + { + "step": "Introduction to CRM" + }, + { + "step": "Create Lead" + }, + { + "step": "Create Opportunity" + }, + { + "step": "Create and Send Quotation" + } + ], + "subtitle": "Lead, Opportunity, Customer, and more.", + "success_message": "The CRM Module is all set up!", + "title": "Let's Set Up Your CRM." +} \ No newline at end of file diff --git a/erpnext/crm/onboarding/crm/crm.json b/erpnext/crm/onboarding/crm/crm.json new file mode 100644 index 0000000000..016a8307dd --- /dev/null +++ b/erpnext/crm/onboarding/crm/crm.json @@ -0,0 +1,45 @@ +{ + "allow_roles": [ + { + "role": "Sales Master Manager" + }, + { + "role": "Administrator" + }, + { + "role": "Sales Manager" + } + ], + "creation": "2020-05-09 23:42:50.901548", + "docstatus": 0, + "doctype": "Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/CRM", + "idx": 0, + "is_complete": 0, + "modified": "2020-05-09 23:42:50.901548", + "modified_by": "Administrator", + "module": "CRM", + "name": "CRM", + "owner": "Administrator", + "steps": [ + { + "step": "Introduction to CRM" + }, + { + "step": "Start Campaign" + }, + { + "step": "Create Lead" + }, + { + "step": "Convert Lead to Customer" + }, + { + "step": "Create and Send Quotation" + } + ], + "subtitle": "Campaign, Lead, Opportunity, Customer and more", + "success_message": "CRM Module is all setup!", + "title": "Let's Setup Your CRM", + "user_can_dismiss": 1 +} \ No newline at end of file diff --git a/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json b/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json new file mode 100644 index 0000000000..78f7e4de9c --- /dev/null +++ b/erpnext/crm/onboarding_step/create_and_send_quotation/create_and_send_quotation.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-09 23:42:46.592075", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-28 21:07:11.461172", + "modified_by": "Administrator", + "name": "Create and Send Quotation", + "owner": "Administrator", + "reference_document": "Quotation", + "show_full_form": 1, + "title": "Create and Send Quotation", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/crm/onboarding_step/create_lead/create_lead.json b/erpnext/crm/onboarding_step/create_lead/create_lead.json new file mode 100644 index 0000000000..c45e8b036c --- /dev/null +++ b/erpnext/crm/onboarding_step/create_lead/create_lead.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-09 23:40:25.192503", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-28 21:07:01.373403", + "modified_by": "Administrator", + "name": "Create Lead", + "owner": "Administrator", + "reference_document": "Lead", + "show_full_form": 1, + "title": "Create Lead", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json new file mode 100644 index 0000000000..9f996d9e2b --- /dev/null +++ b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:38:27.496696", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 17:38:27.496696", + "modified_by": "Administrator", + "name": "Create Opportunity", + "owner": "Administrator", + "reference_document": "Opportunity", + "show_full_form": 0, + "title": "Create Opportunity", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json b/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json new file mode 100644 index 0000000000..fa26921ae2 --- /dev/null +++ b/erpnext/crm/onboarding_step/introduction_to_crm/introduction_to_crm.json @@ -0,0 +1,19 @@ +{ + "action": "Watch Video", + "creation": "2020-05-09 23:37:08.926812", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 17:28:16.448676", + "modified_by": "Administrator", + "name": "Introduction to CRM", + "owner": "Administrator", + "show_full_form": 0, + "title": "Introduction to CRM", + "validate_action": 1, + "video_url": "https://www.youtube.com/watch?v=o9XCSZHJfpA" +} \ No newline at end of file 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 6172a75fdd..8fe16a2f4c 100644 --- a/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.py +++ b/erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.py @@ -17,7 +17,8 @@ def get_columns(): { "fieldname": "lead_owner", "label": _("Lead Owner"), - "fieldtype": "Data", + "fieldtype": "Link", + "options": "User", "width": "130" }, { @@ -68,4 +69,4 @@ def get_columns(): "fieldtype": "Float", "width": "100" } - ] \ No newline at end of file + ] diff --git a/erpnext/education/desk_page/education/education.json b/erpnext/education/desk_page/education/education.json index fc2697f0d7..b341ec4b99 100644 --- a/erpnext/education/desk_page/education/education.json +++ b/erpnext/education/desk_page/education/education.json @@ -64,6 +64,11 @@ "hidden": 0, "label": "Assessment Reports", "links": "[\n {\n \"dependencies\": [\n \"Assessment Result\"\n ],\n \"doctype\": \"Assessment Result\",\n \"is_query_report\": true,\n \"label\": \"Course wise Assessment Report\",\n \"name\": \"Course wise Assessment Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Assessment Result\"\n ],\n \"doctype\": \"Assessment Result\",\n \"is_query_report\": true,\n \"label\": \"Final Assessment Grades\",\n \"name\": \"Final Assessment Grades\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Assessment Plan\"\n ],\n \"doctype\": \"Assessment Plan\",\n \"is_query_report\": true,\n \"label\": \"Assessment Plan Status\",\n \"name\": \"Assessment Plan Status\",\n \"type\": \"report\"\n },\n {\n \"label\": \"Student Report Generation Tool\",\n \"name\": \"Student Report Generation Tool\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Reports", + "links": "[\n {\n \"dependencies\": [\n \"Fees\"\n ],\n \"doctype\": \"Fees\",\n \"is_query_report\": true,\n \"label\": \"Student Fee Collection\",\n \"name\": \"Student Fee Collection\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Student Monthly Attendance Sheet\",\n \"name\": \"Student Monthly Attendance Sheet\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Absent Student Report\",\n \"name\": \"Absent Student Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Program Enrollment\"\n ],\n \"doctype\": \"Program Enrollment\",\n \"is_query_report\": true,\n \"label\": \"Student and Guardian Contact Details\",\n \"name\": \"Student and Guardian Contact Details\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Student Batch-Wise Attendance\",\n \"name\": \"Student Batch-Wise Attendance\",\n \"type\": \"report\"\n }\n]" } ], "category": "Domains", @@ -77,7 +82,7 @@ "idx": 0, "is_standard": 1, "label": "Education", - "modified": "2020-04-01 11:28:51.011309", + "modified": "2020-05-22 01:09:13.058482", "modified_by": "Administrator", "module": "Education", "name": "Education", diff --git a/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py b/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py new file mode 100644 index 0000000000..c36dfb11b5 --- /dev/null +++ b/erpnext/education/doctype/assessment_plan/assessment_plan_dashboard.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'assessment_plan', + 'non_standard_fieldnames': { + }, + 'transactions': [ + { + 'label': _('Assessment'), + 'items': ['Assessment Result'] + } + ] + } \ No newline at end of file diff --git a/erpnext/education/doctype/course/course_dashboard.py b/erpnext/education/doctype/course/course_dashboard.py new file mode 100644 index 0000000000..752af29a9d --- /dev/null +++ b/erpnext/education/doctype/course/course_dashboard.py @@ -0,0 +1,25 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'course', + 'non_standard_fieldnames': { + }, + 'transactions': [ + { + 'label': _('Course'), + 'items': ['Course Enrollment', 'Course Schedule'] + }, + { + 'label': _('Student'), + 'items': ['Student Group'] + }, + { + 'label': _('Assessment'), + 'items': ['Assessment Plan'] + }, + ] + } \ No newline at end of file diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.json b/erpnext/education/doctype/fee_schedule/fee_schedule.json index 1e98709b2b..791831810a 100644 --- a/erpnext/education/doctype/fee_schedule/fee_schedule.json +++ b/erpnext/education/doctype/fee_schedule/fee_schedule.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2017-07-18 15:21:21.527136", @@ -7,6 +8,7 @@ "engine": "InnoDB", "field_order": [ "fee_structure", + "posting_date", "due_date", "naming_series", "fee_creation_status", @@ -259,10 +261,18 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "reqd": 1 } ], "is_submittable": 1, - "modified": "2019-05-26 09:10:34.522409", + "links": [], + "modified": "2020-05-15 08:39:20.682837", "modified_by": "Administrator", "module": "Education", "name": "Fee Schedule", diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.py b/erpnext/education/doctype/fee_schedule/fee_schedule.py index a42800a80d..1543acdca9 100644 --- a/erpnext/education/doctype/fee_schedule/fee_schedule.py +++ b/erpnext/education/doctype/fee_schedule/fee_schedule.py @@ -87,6 +87,7 @@ def generate_fee(fee_schedule): } } }) + fees_doc.posting_date = doc.posting_date fees_doc.student = student.student fees_doc.student_name = student.student_name fees_doc.program = student.program diff --git a/erpnext/education/doctype/fee_structure/fee_structure.js b/erpnext/education/doctype/fee_structure/fee_structure.js index 7606565fad..f09d2efcb9 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure.js +++ b/erpnext/education/doctype/fee_structure/fee_structure.js @@ -9,6 +9,14 @@ frappe.ui.form.on('Fee Structure', { }, onload: function(frm) { + frm.set_query("academic_term", function() { + return { + "filters": { + "academic_year": frm.doc.academic_year + } + }; + }); + frm.set_query("receivable_account", function(doc) { return { filters: { diff --git a/erpnext/education/doctype/fee_structure/fee_structure.json b/erpnext/education/doctype/fee_structure/fee_structure.json index 8ff6851d90..67e46372f8 100644 --- a/erpnext/education/doctype/fee_structure/fee_structure.json +++ b/erpnext/education/doctype/fee_structure/fee_structure.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "naming_series:", @@ -11,8 +12,8 @@ "program", "student_category", "column_break_2", - "academic_term", "academic_year", + "academic_term", "section_break_4", "components", "section_break_6", @@ -157,7 +158,8 @@ ], "icon": "fa fa-flag", "is_submittable": 1, - "modified": "2019-05-26 09:04:17.765758", + "links": [], + "modified": "2020-06-16 15:34:57.295010", "modified_by": "Administrator", "module": "Education", "name": "Fee Structure", diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js index 17ef44954b..867866fbf1 100644 --- a/erpnext/education/doctype/fees/fees.js +++ b/erpnext/education/doctype/fees/fees.js @@ -55,14 +55,15 @@ frappe.ui.form.on("Fees", { frm.set_df_property('posting_date', 'read_only', 1); frm.set_df_property('posting_time', 'read_only', 1); } - if(frm.doc.docstatus===1) { + if(frm.doc.docstatus > 0) { frm.add_custom_button(__('Accounting Ledger'), function() { frappe.route_options = { voucher_no: frm.doc.name, from_date: frm.doc.posting_date, - to_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), company: frm.doc.company, - group_by_voucher: false + group_by: '', + show_cancelled_entries: frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); }, __("View")); diff --git a/erpnext/education/doctype/program_course/program_course.json b/erpnext/education/doctype/program_course/program_course.json index a24e88a861..940358e4e9 100644 --- a/erpnext/education/doctype/program_course/program_course.json +++ b/erpnext/education/doctype/program_course/program_course.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2015-09-07 14:37:01.886859", "doctype": "DocType", "editable_grid": 1, @@ -16,26 +17,33 @@ "in_list_view": 1, "label": "Course", "options": "Course", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, - { + { + "fetch_from": "course.course_name", "fieldname": "course_name", "fieldtype": "Data", "in_list_view": 1, "label": "Course Name", - "fetch_from": "course.course_name", - "read_only":1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "required", "fieldtype": "Check", "in_list_view": 1, - "label": "Mandatory" + "label": "Mandatory", + "show_days": 1, + "show_seconds": 1 } ], "istable": 1, - "modified": "2019-06-12 12:42:12.845972", + "links": [], + "modified": "2020-06-09 18:56:10.213241", "modified_by": "Administrator", "module": "Education", "name": "Program Course", @@ -45,4 +53,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/education/doctype/student_admission/student_admission.json b/erpnext/education/doctype/student_admission/student_admission.json index b3c10d4331..1096888d4d 100644 --- a/erpnext/education/doctype/student_admission/student_admission.json +++ b/erpnext/education/doctype/student_admission/student_admission.json @@ -1,398 +1,119 @@ { - "allow_copy": 0, - "allow_guest_to_view": 1, - "allow_import": 0, - "allow_rename": 1, - "autoname": "", - "beta": 0, - "creation": "2016-09-13 03:05:27.154713", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "allow_guest_to_view": 1, + "allow_rename": 1, + "creation": "2016-09-13 03:05:27.154713", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "route", + "column_break_3", + "academic_year", + "admission_start_date", + "admission_end_date", + "published", + "enable_admission_application", + "section_break_5", + "program_details", + "introduction" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "route", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Route", - "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, + "fieldname": "route", + "fieldtype": "Data", + "label": "Route", + "no_copy": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "application_form_route", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Application Form Route", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "academic_year", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Academic Year", + "no_copy": 1, + "options": "Academic Year", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "academic_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Academic Year", - "length": 0, - "no_copy": 1, - "options": "Academic Year", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "admission_start_date", + "fieldtype": "Date", + "label": "Admission Start Date", + "no_copy": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "admission_start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Admission Start Date", - "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, - "unique": 0 - }, + "fieldname": "admission_end_date", + "fieldtype": "Date", + "label": "Admission End Date", + "no_copy": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "admission_end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Admission End Date", - "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, - "unique": 0 - }, + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "label": "Publish on website" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "published", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Publish on website", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Eligibility and Details" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Eligibility and Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "program_details", + "fieldtype": "Table", + "label": "Eligibility and Details", + "options": "Student Admission Program" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "program_details", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Eligibility and Details", - "length": 0, - "no_copy": 0, - "options": "Student Admission Program", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "introduction", + "fieldtype": "Text Editor", + "label": "Introduction" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "introduction", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Introduction", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "default": "0", + "fieldname": "enable_admission_application", + "fieldtype": "Check", + "label": "Enable Admission Application" } - ], - "has_web_view": 1, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_published_field": "published", - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-11-10 18:57:34.570376", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Admission", - "name_case": "", - "owner": "Administrator", + ], + "has_web_view": 1, + "is_published_field": "published", + "links": [], + "modified": "2020-06-15 20:18:38.591626", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Admission", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Academics User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Academics User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "route": "admissions", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "title", - "track_changes": 0, - "track_seen": 0 + ], + "restrict_to_domain": "Education", + "route": "admissions", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title" } \ No newline at end of file diff --git a/erpnext/education/doctype/student_admission/templates/student_admission.html b/erpnext/education/doctype/student_admission/templates/student_admission.html index 25afaca84d..e5a9ead31e 100644 --- a/erpnext/education/doctype/student_admission/templates/student_admission.html +++ b/erpnext/education/doctype/student_admission/templates/student_admission.html @@ -43,8 +43,8 @@ Program/Std. - Minumum Age(DOB) - Maximum Age(DOB) + Minumum Age + Maximum Age Application Fee @@ -52,8 +52,8 @@ {% for row in program_details %} {{ row.program }} - {{ row.minimum_age }} - {{ row.maximum_age }} + {{ row.min_age }} + {{ row.max_age }} {{ row.application_fee }} {% endfor %} @@ -61,12 +61,11 @@ {% endif %} - - {%- if application_form_route -%} + {%- if doc.enable_admission_application -%}

+ href='/student-applicant?new=1&student_admission={{doc.name}}'> {{ _("Apply Now") }}

{% endif %} diff --git a/erpnext/education/doctype/student_admission/test_student_admission.js b/erpnext/education/doctype/student_admission/test_student_admission.js index ed794b2482..3a0bb0b2f2 100644 --- a/erpnext/education/doctype/student_admission/test_student_admission.js +++ b/erpnext/education/doctype/student_admission/test_student_admission.js @@ -11,7 +11,7 @@ QUnit.test('Test: Student Admission', function(assert) { {admission_start_date: '2016-04-20'}, {admission_end_date: '2016-05-31'}, {title: '2016-17 Admissions'}, - {application_form_route: 'student-applicant'}, + {enable_admission_application: 1}, {introduction: 'Test intro'}, {program_details: [ [ @@ -28,7 +28,7 @@ QUnit.test('Test: Student Admission', function(assert) { assert.ok(cur_frm.doc.admission_start_date == '2016-04-20'); assert.ok(cur_frm.doc.admission_end_date == '2016-05-31'); assert.ok(cur_frm.doc.title == '2016-17 Admissions'); - assert.ok(cur_frm.doc.application_form_route == 'student-applicant'); + assert.ok(cur_frm.doc.enable_admission_application == 1); assert.ok(cur_frm.doc.introduction == 'Test intro'); assert.ok(cur_frm.doc.program_details[0].program == 'Standard Test', 'Program correctly selected'); assert.ok(cur_frm.doc.program_details[0].application_fee == 1000); diff --git a/erpnext/education/doctype/student_admission_program/student_admission_program.json b/erpnext/education/doctype/student_admission_program/student_admission_program.json index 97b1bba421..e9f041e101 100644 --- a/erpnext/education/doctype/student_admission_program/student_admission_program.json +++ b/erpnext/education/doctype/student_admission_program/student_admission_program.json @@ -1,237 +1,77 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2017-09-15 12:59:43.207923", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-09-15 12:59:43.207923", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "program", + "min_age", + "max_age", + "column_break_4", + "application_fee", + "applicant_naming_series" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "program", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Program", - "length": 0, - "no_copy": 0, - "options": "Program", - "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": "program", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Program", + "options": "Program", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "minimum_age", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Minimum Age", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maximum_age", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Maximum Age", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "application_fee", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Application Fee", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "applicant_naming_series", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Naming Series (for Student Applicant)", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "application_fee", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Application Fee", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "min_age", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Minimum Age", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "applicant_naming_series", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Naming Series (for Student Applicant)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "max_age", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Maximum Age", + "show_days": 1, + "show_seconds": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-11-04 03:37:17.408427", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Admission Program", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "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": "2020-06-10 23:06:30.037404", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Admission Program", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "restrict_to_domain": "Education", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/education/doctype/student_applicant/student_applicant.py b/erpnext/education/doctype/student_applicant/student_applicant.py index ab947807dd..211348201e 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.py +++ b/erpnext/education/doctype/student_applicant/student_applicant.py @@ -6,7 +6,7 @@ from __future__ import print_function, unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import getdate +from frappe.utils import getdate, add_years, nowdate, date_diff class StudentApplicant(Document): def autoname(self): @@ -31,6 +31,7 @@ class StudentApplicant(Document): def validate(self): self.validate_dates() self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) + if self.student_admission and self.program and self.date_of_birth: self.validation_from_student_admission() @@ -48,16 +49,16 @@ class StudentApplicant(Document): frappe.throw(_("Please select Student Admission which is mandatory for the paid student applicant")) def validation_from_student_admission(self): + student_admission = get_student_admission_data(self.student_admission, self.program) - # different validation for minimum and maximum age so that either min/max can also work independently. - if student_admission and student_admission.minimum_age and \ - getdate(student_admission.minimum_age) < getdate(self.date_of_birth): - frappe.throw(_("Not eligible for the admission in this program as per DOB")) + 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.maximum_age and \ - getdate(student_admission.maximum_age) > getdate(self.date_of_birth): - frappe.throw(_("Not eligible for the admission in this program as per DOB")) + 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): @@ -65,10 +66,12 @@ class StudentApplicant(Document): def get_student_admission_data(student_admission, program): + student_admission = frappe.db.sql("""select sa.admission_start_date, sa.admission_end_date, - sap.program, sap.minimum_age, sap.maximum_age, sap.applicant_naming_series + 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) + if student_admission: return student_admission[0] else: diff --git a/erpnext/education/report/absent_student_report/absent_student_report.json b/erpnext/education/report/absent_student_report/absent_student_report.json index 0d5eebabf8..92ad860cc6 100644 --- a/erpnext/education/report/absent_student_report/absent_student_report.json +++ b/erpnext/education/report/absent_student_report/absent_student_report.json @@ -1,20 +1,21 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-05-13 14:04:03", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-11-10 19:42:36.457449", - "modified_by": "Administrator", - "module": "Education", - "name": "Absent Student Report", - "owner": "Administrator", - "ref_doctype": "Student Attendance", - "report_name": "Absent Student Report", - "report_type": "Script Report", + "add_total_row": 0, + "creation": "2013-05-13 14:04:03", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 3, + "is_standard": "Yes", + "modified": "2020-06-24 17:16:40.251116", + "modified_by": "Administrator", + "module": "Education", + "name": "Absent Student Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Student Attendance", + "report_name": "Absent Student Report", + "report_type": "Script Report", "roles": [ { "role": "Academics User" diff --git a/erpnext/education/report/assessment_plan_status/assessment_plan_status.json b/erpnext/education/report/assessment_plan_status/assessment_plan_status.json index 3000bec1f8..cbca648d57 100644 --- a/erpnext/education/report/assessment_plan_status/assessment_plan_status.json +++ b/erpnext/education/report/assessment_plan_status/assessment_plan_status.json @@ -1,20 +1,21 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2017-11-09 15:07:30.404428", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2017-11-28 18:35:44.903665", - "modified_by": "Administrator", - "module": "Education", - "name": "Assessment Plan Status", - "owner": "Administrator", - "ref_doctype": "Assessment Plan", - "report_name": "Assessment Plan Status", - "report_type": "Script Report", + "add_total_row": 0, + "creation": "2017-11-09 15:07:30.404428", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-06-24 17:16:02.027410", + "modified_by": "Administrator", + "module": "Education", + "name": "Assessment Plan Status", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Assessment Plan", + "report_name": "Assessment Plan Status", + "report_type": "Script Report", "roles": [ { "role": "Academics User" diff --git a/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.json b/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.json index 61976b4508..416db9d00f 100644 --- a/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.json +++ b/erpnext/education/report/course_wise_assessment_report/course_wise_assessment_report.json @@ -1,24 +1,26 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2017-05-05 14:46:13.776133", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2018-02-08 15:11:24.904628", - "modified_by": "Administrator", - "module": "Education", - "name": "Course wise Assessment Report", - "owner": "Administrator", - "ref_doctype": "Assessment Result", - "report_name": "Course wise Assessment Report", - "report_type": "Script Report", + "add_total_row": 0, + "creation": "2017-05-05 14:46:13.776133", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-06-24 17:15:15.477530", + "modified_by": "Administrator", + "module": "Education", + "name": "Course wise Assessment Report", + "owner": "Administrator", + "prepared_report": 0, + "query": "", + "ref_doctype": "Assessment Result", + "report_name": "Course wise Assessment Report", + "report_type": "Script Report", "roles": [ { "role": "Instructor" - }, + }, { "role": "Education Manager" } diff --git a/erpnext/education/report/final_assessment_grades/final_assessment_grades.json b/erpnext/education/report/final_assessment_grades/final_assessment_grades.json index 4d444b46ce..6a23494768 100644 --- a/erpnext/education/report/final_assessment_grades/final_assessment_grades.json +++ b/erpnext/education/report/final_assessment_grades/final_assessment_grades.json @@ -1,24 +1,25 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2018-01-22 17:04:43.412054", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2019-02-08 15:11:35.339434", - "modified_by": "Administrator", - "module": "Education", - "name": "Final Assessment Grades", - "owner": "Administrator", - "ref_doctype": "Assessment Result", - "report_name": "Final Assessment Grades", - "report_type": "Script Report", + "add_total_row": 0, + "creation": "2018-01-22 17:04:43.412054", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-06-24 17:13:35.373756", + "modified_by": "Administrator", + "module": "Education", + "name": "Final Assessment Grades", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Assessment Result", + "report_name": "Final Assessment Grades", + "report_type": "Script Report", "roles": [ { "role": "Instructor" - }, + }, { "role": "Education Manager" } diff --git a/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.json b/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.json index fe7d1586c8..fa9be65681 100644 --- a/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.json +++ b/erpnext/education/report/student_and_guardian_contact_details/student_and_guardian_contact_details.json @@ -1,24 +1,25 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2017-03-27 17:47:16.831433", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2017-11-10 19:42:30.300729", - "modified_by": "Administrator", - "module": "Education", - "name": "Student and Guardian Contact Details", - "owner": "Administrator", - "ref_doctype": "Program Enrollment", - "report_name": "Student and Guardian Contact Details", - "report_type": "Script Report", + "add_total_row": 0, + "creation": "2017-03-27 17:47:16.831433", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-06-24 17:16:50.639488", + "modified_by": "Administrator", + "module": "Education", + "name": "Student and Guardian Contact Details", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Program Enrollment", + "report_name": "Student and Guardian Contact Details", + "report_type": "Script Report", "roles": [ { "role": "Instructor" - }, + }, { "role": "Academics User" } diff --git a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.json b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.json index eb547b7102..8baf8f9fe0 100644 --- a/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.json +++ b/erpnext/education/report/student_batch_wise_attendance/student_batch_wise_attendance.json @@ -1,20 +1,21 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2016-11-28 22:07:03.859124", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2017-11-10 19:41:12.328346", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Batch-Wise Attendance", - "owner": "Administrator", - "ref_doctype": "Student Attendance", - "report_name": "Student Batch-Wise Attendance", - "report_type": "Script Report", + "add_total_row": 0, + "creation": "2016-11-28 22:07:03.859124", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 2, + "is_standard": "Yes", + "modified": "2020-06-24 17:16:59.823709", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Batch-Wise Attendance", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Student Attendance", + "report_name": "Student Batch-Wise Attendance", + "report_type": "Script Report", "roles": [ { "role": "Academics User" diff --git a/erpnext/education/report/student_fee_collection/student_fee_collection.json b/erpnext/education/report/student_fee_collection/student_fee_collection.json index eb945cfffb..8deb865ebc 100644 --- a/erpnext/education/report/student_fee_collection/student_fee_collection.json +++ b/erpnext/education/report/student_fee_collection/student_fee_collection.json @@ -1,21 +1,22 @@ { - "add_total_row": 0, - "creation": "2016-06-22 02:58:41.024538", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2018-12-17 16:46:46.176620", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Fee Collection", - "owner": "Administrator", - "prepared_report": 0, - "query": "SELECT\n student as \"Student:Link/Student:200\",\n student_name as \"Student Name::200\",\n sum(grand_total) - sum(outstanding_amount) as \"Paid Amount:Currency:150\",\n sum(outstanding_amount) as \"Outstanding Amount:Currency:150\",\n sum(grand_total) as \"Grand Total:Currency:150\"\nFROM\n `tabFees` \nGROUP BY\n student", - "ref_doctype": "Fees", - "report_name": "Student Fee Collection", - "report_type": "Query Report", + "add_total_row": 0, + "creation": "2016-06-22 02:58:41.024538", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 3, + "is_standard": "Yes", + "modified": "2020-06-24 17:14:39.452551", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Fee Collection", + "owner": "Administrator", + "prepared_report": 0, + "query": "SELECT\n student as \"Student:Link/Student:200\",\n student_name as \"Student Name::200\",\n sum(grand_total) - sum(outstanding_amount) as \"Paid Amount:Currency:150\",\n sum(outstanding_amount) as \"Outstanding Amount:Currency:150\",\n sum(grand_total) as \"Grand Total:Currency:150\"\nFROM\n `tabFees` \nGROUP BY\n student", + "ref_doctype": "Fees", + "report_name": "Student Fee Collection", + "report_type": "Query Report", "roles": [ { "role": "Academics User" diff --git a/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.json b/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.json index e10f190e1c..1423d4fee1 100644 --- a/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.json +++ b/erpnext/education/report/student_monthly_attendance_sheet/student_monthly_attendance_sheet.json @@ -1,20 +1,21 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-05-13 14:04:03", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-11-10 19:42:43.376658", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Monthly Attendance Sheet", - "owner": "Administrator", - "ref_doctype": "Student Attendance", - "report_name": "Student Monthly Attendance Sheet", - "report_type": "Script Report", + "add_total_row": 0, + "creation": "2013-05-13 14:04:03", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 3, + "is_standard": "Yes", + "modified": "2020-06-24 17:16:13.307053", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Monthly Attendance Sheet", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Student Attendance", + "report_name": "Student Monthly Attendance Sheet", + "report_type": "Script Report", "roles": [ { "role": "Academics User" diff --git a/erpnext/education/web_form/student_applicant/student_applicant.json b/erpnext/education/web_form/student_applicant/student_applicant.json index b1ad754c32..1810f07a05 100644 --- a/erpnext/education/web_form/student_applicant/student_applicant.json +++ b/erpnext/education/web_form/student_applicant/student_applicant.json @@ -1,200 +1,248 @@ { - "accept_payment": 0, - "allow_comments": 0, - "allow_delete": 0, - "allow_edit": 1, - "allow_incomplete": 0, - "allow_multiple": 1, - "allow_print": 0, - "amount": 0.0, - "amount_based_on_field": 0, - "creation": "2016-09-22 13:10:10.792735", - "doc_type": "Student Applicant", - "docstatus": 0, - "doctype": "Web Form", - "idx": 0, - "is_standard": 1, - "login_required": 1, - "max_attachment_size": 0, - "modified": "2017-02-21 05:44:46.022738", - "modified_by": "Administrator", - "module": "Education", - "name": "student-applicant", - "owner": "Administrator", - "payment_button_label": "Buy Now", - "published": 1, - "route": "student-applicant", - "show_sidebar": 1, - "sidebar_items": [], - "success_url": "/student-applicant", - "title": "Student Applicant", + "accept_payment": 0, + "allow_comments": 0, + "allow_delete": 0, + "allow_edit": 1, + "allow_incomplete": 0, + "allow_multiple": 1, + "allow_print": 0, + "amount": 0.0, + "amount_based_on_field": 0, + "creation": "2016-09-22 13:10:10.792735", + "doc_type": "Student Applicant", + "docstatus": 0, + "doctype": "Web Form", + "idx": 0, + "is_standard": 1, + "login_required": 1, + "max_attachment_size": 0, + "modified": "2020-06-11 22:53:45.875310", + "modified_by": "Administrator", + "module": "Education", + "name": "student-applicant", + "owner": "Administrator", + "payment_button_label": "Buy Now", + "published": 1, + "route": "student-applicant", + "route_to_success_link": 0, + "show_attachments": 0, + "show_in_grid": 0, + "show_sidebar": 1, + "sidebar_items": [], + "success_url": "/student-applicant", + "title": "Student Applicant", "web_form_fields": [ { - "fieldname": "first_name", - "fieldtype": "Data", - "hidden": 0, - "label": "First Name", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 1 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "first_name", + "fieldtype": "Data", + "hidden": 0, + "label": "First Name", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, { - "fieldname": "middle_name", - "fieldtype": "Data", - "hidden": 0, - "label": "Middle Name", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "middle_name", + "fieldtype": "Data", + "hidden": 0, + "label": "Middle Name", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "last_name", - "fieldtype": "Data", - "hidden": 0, - "label": "Last Name", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "last_name", + "fieldtype": "Data", + "hidden": 0, + "label": "Last Name", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "image", - "fieldtype": "Data", - "hidden": 0, - "label": "Image", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "image", + "fieldtype": "Data", + "hidden": 0, + "label": "Image", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "program", - "fieldtype": "Link", - "hidden": 0, - "label": "Program", - "max_length": 0, - "max_value": 0, - "options": "Program", - "read_only": 0, - "reqd": 1 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "program", + "fieldtype": "Link", + "hidden": 0, + "label": "Program", + "max_length": 0, + "max_value": 0, + "options": "Program", + "read_only": 0, + "reqd": 1, + "show_in_filter": 0 + }, { - "fieldname": "academic_year", - "fieldtype": "Link", - "hidden": 0, - "label": "Academic Year", - "max_length": 0, - "max_value": 0, - "options": "Academic Year", - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "academic_year", + "fieldtype": "Link", + "hidden": 0, + "label": "Academic Year", + "max_length": 0, + "max_value": 0, + "options": "Academic Year", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "date_of_birth", - "fieldtype": "Date", - "hidden": 0, - "label": "Date of Birth", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "date_of_birth", + "fieldtype": "Date", + "hidden": 0, + "label": "Date of Birth", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "blood_group", - "fieldtype": "Select", - "hidden": 0, - "label": "Blood Group", - "max_length": 0, - "max_value": 0, - "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-", - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "blood_group", + "fieldtype": "Select", + "hidden": 0, + "label": "Blood Group", + "max_length": 0, + "max_value": 0, + "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "student_email_id", - "fieldtype": "Data", - "hidden": 0, - "label": "Student Email ID", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "student_email_id", + "fieldtype": "Data", + "hidden": 0, + "label": "Student Email ID", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "student_mobile_number", - "fieldtype": "Data", - "hidden": 0, - "label": "Student Mobile Number", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "student_mobile_number", + "fieldtype": "Data", + "hidden": 0, + "label": "Student Mobile Number", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "default": "INDIAN", - "fieldname": "nationality", - "fieldtype": "Data", - "hidden": 0, - "label": "Nationality", - "max_length": 0, - "max_value": 0, - "options": "", - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "default": "INDIAN", + "fieldname": "nationality", + "fieldtype": "Data", + "hidden": 0, + "label": "Nationality", + "max_length": 0, + "max_value": 0, + "options": "", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "address_line_1", - "fieldtype": "Data", - "hidden": 0, - "label": "Address Line 1", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "address_line_1", + "fieldtype": "Data", + "hidden": 0, + "label": "Address Line 1", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "address_line_2", - "fieldtype": "Data", - "hidden": 0, - "label": "Address Line 2", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "address_line_2", + "fieldtype": "Data", + "hidden": 0, + "label": "Address Line 2", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "pincode", - "fieldtype": "Data", - "hidden": 0, - "label": "Pincode", - "max_length": 0, - "max_value": 0, - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "pincode", + "fieldtype": "Data", + "hidden": 0, + "label": "Pincode", + "max_length": 0, + "max_value": 0, + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "guardians", - "fieldtype": "Table", - "hidden": 0, - "label": "Guardians", - "max_length": 0, - "max_value": 0, - "options": "Student Guardian", - "read_only": 0, - "reqd": 0 - }, + "allow_read_on_all_link_options": 0, + "fieldname": "guardians", + "fieldtype": "Table", + "hidden": 0, + "label": "Guardians", + "max_length": 0, + "max_value": 0, + "options": "Student Guardian", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, { - "fieldname": "siblings", - "fieldtype": "Table", - "hidden": 0, - "label": "Siblings", - "max_length": 0, - "max_value": 0, - "options": "Student Sibling", - "read_only": 0, - "reqd": 0 + "allow_read_on_all_link_options": 0, + "fieldname": "siblings", + "fieldtype": "Table", + "hidden": 0, + "label": "Siblings", + "max_length": 0, + "max_value": 0, + "options": "Student Sibling", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "fieldname": "student_admission", + "fieldtype": "Link", + "hidden": 0, + "label": "Student Admission", + "max_length": 0, + "max_value": 0, + "options": "Student Admission", + "read_only": 0, + "reqd": 0, + "show_in_filter": 0 } ] } \ No newline at end of file diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index ca0e1609cb..d59f909298 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -95,10 +95,10 @@ def create_sales_order(shopify_order, shopify_settings, company=None): items = get_order_items(shopify_order.get("line_items"), shopify_settings) if not items: - message = 'Following items are exists in order but relevant record not found in 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=e, rollback=True) + make_shopify_log(status="Error", exception=message, rollback=True) return '' @@ -241,14 +241,17 @@ def get_order_taxes(shopify_order, 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""" for shipping_charge in shipping_lines: - 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 + }) return taxes diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py index 618865200c..6dedaa8c53 100644 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -49,12 +49,13 @@ def _order(*args, **kwargs): if event == "created": 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") - link_customer_and_address(raw_billing_data, customer_name) + link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name) 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, customer_name): +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}) if not customer_exists: @@ -68,38 +69,80 @@ def link_customer_and_address(raw_billing_data, customer_name): customer.customer_name = customer_name customer.woocommerce_email = customer_woo_com_email customer.flags.ignore_mandatory = True - customer.save() + customer.save() if customer_exists: frappe.rename_doc("Customer", old_name, customer_name) - address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email}) + for address_type in ("Billing", "Shipping",): + try: + address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email, "address_type": address_type}) + rename_address(address, customer) + except ( + frappe.DoesNotExistError, + frappe.DuplicateEntryError, + frappe.ValidationError, + ): + pass else: - address = frappe.new_doc("Address") + create_address(raw_billing_data, customer, "Billing") + create_address(raw_shipping_data, customer, "Shipping") + create_contact(raw_billing_data, customer) - address.address_line1 = raw_billing_data.get("address_1", "Not Provided") - address.address_line2 = raw_billing_data.get("address_2", "Not Provided") - address.city = raw_billing_data.get("city", "Not Provided") - address.woocommerce_email = customer_woo_com_email - address.address_type = "Billing" - address.country = frappe.get_value("Country", {"code": raw_billing_data.get("country", "IN").lower()}) - address.state = raw_billing_data.get("state") - address.pincode = raw_billing_data.get("postcode") - address.phone = raw_billing_data.get("phone") - address.email_id = customer_woo_com_email +def create_contact(data, customer): + email = data.get("email", None) + phone = data.get("phone", None) + + if not email and not phone: + return + + contact = frappe.new_doc("Contact") + contact.first_name = data.get("first_name") + contact.last_name = data.get("last_name") + contact.is_primary_contact = 1 + contact.is_billing_contact = 1 + + if phone: + contact.add_phone(phone, is_primary_mobile_no=1, is_primary_phone=1) + + if email: + contact.add_email(email, is_primary=1) + + 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") + + address.address_line1 = raw_data.get("address_1", "Not Provided") + address.address_line2 = raw_data.get("address_2", "Not Provided") + address.city = raw_data.get("city", "Not Provided") + address.woocommerce_email = customer.woocommerce_email + address.address_type = address_type + address.country = frappe.get_value("Country", {"code": raw_data.get("country", "IN").lower()}) + address.state = raw_data.get("state") + 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.customer_name + "link_name": customer.name }) + address.flags.ignore_mandatory = True - address = address.save() + address.save() - if customer_exists: - old_address_title = address.name - new_address_title = customer.customer_name + "-billing" - address.address_title = customer.customer_name - address.save() +def rename_address(address, customer): + old_address_title = address.name + new_address_title = customer.name + "-" + address.address_type + address.address_title = customer.customer_name + address.save() - frappe.rename_doc("Address", old_address_title, new_address_title) + 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: @@ -111,7 +154,7 @@ def link_items(items_list, woocommerce_settings, sys_lang): else: #Create Item item = frappe.new_doc("Item") - + item.item_name = item_data.get("name") item.item_code = _("woocommerce - {0}", sys_lang).format(item_data.get("product_id")) item.woocommerce_id = item_data.get("product_id") @@ -145,7 +188,8 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l 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): + 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"): @@ -171,7 +215,7 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l 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", diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index a7062239c3..c3371ed5df 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -124,10 +124,11 @@ def add_account_subtype(account_subtype): @frappe.whitelist() def sync_transactions(bank, bank_account): - - last_sync_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date") - if last_sync_date: - start_date = formatdate(last_sync_date, "YYYY-MM-dd") + '''Sync transactions based on the last integration date as the start date, after the sync is completed + add the transaction date of the oldest transaction as the last integration date''' + last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date") + if last_transaction_date: + start_date = formatdate(last_transaction_date, "YYYY-MM-dd") else: start_date = formatdate(add_months(today(), -12), "YYYY-MM-dd") end_date = formatdate(today(), "YYYY-MM-dd") @@ -139,12 +140,14 @@ def sync_transactions(bank, bank_account): for transaction in reversed(transactions): result += new_bank_transaction(transaction) - frappe.logger().info("Plaid added {} new Bank Transactions from '{}' between {} and {}".format( - len(result), bank_account, start_date, end_date)) + if result: + last_transaction_date = frappe.db.get_value('Bank Transaction', result.pop(), 'date') - frappe.db.set_value("Bank Account", bank_account, "last_integration_date", getdate(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) - return result except Exception: frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json index 8f1b746e5b..5339c99155 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json @@ -133,7 +133,7 @@ "label": "Customer Settings" }, { - "description": "If Shopify not contains a customer in Order, then while syncing Orders, the system will consider default customer for order", + "description": "If Shopify does not have a customer in the order, then while syncing the orders, the system will consider the default customer for the order", "fieldname": "default_customer", "fieldtype": "Link", "label": "Default Customer", @@ -258,7 +258,7 @@ } ], "issingle": 1, - "modified": "2019-09-13 12:32:11.384757", + "modified": "2020-05-28 12:32:11.384757", "modified_by": "umair@erpnext.com", "module": "ERPNext Integrations", "name": "Shopify Settings", @@ -277,4 +277,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py index 64c3b2d273..25ffd28109 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py @@ -8,6 +8,7 @@ import json from frappe import _ from frappe.model.document import Document from frappe.utils import get_request_session +from requests.exceptions import HTTPError from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from erpnext.erpnext_integrations.utils import get_webhook_address from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log @@ -29,19 +30,24 @@ class ShopifySettings(Document): 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/2019-04/webhooks.json', self) + url = get_shopify_url('admin/api/2020-04/webhooks.json', self) for method in webhooks: session = get_request_session() try: - d = session.post(url, data=json.dumps({ + res = session.post(url, data=json.dumps({ "webhook": { "topic": method, "address": get_webhook_address(connector_name='shopify_connection', method='store_request_data'), "format": "json" } }), headers=get_header(self)) - d.raise_for_status() - self.update_webhook_table(method, d.json()) + res.raise_for_status() + self.update_webhook_table(method, res.json()) + + except HTTPError as e: + error_message = res.json().get('errors', e) + make_shopify_log(status="Warning", exception=error_message, rollback=True) + except Exception as e: make_shopify_log(status="Warning", exception=e, rollback=True) @@ -50,13 +56,18 @@ class ShopifySettings(Document): deleted_webhooks = [] for d in self.webhooks: - url = get_shopify_url('admin/api/2019-04/webhooks/{0}.json'.format(d.webhook_id), self) + url = get_shopify_url('admin/api/2020-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) + make_shopify_log(status="Warning", exception=error_message, rollback=True) + except Exception as e: - frappe.log_error(message=frappe.get_traceback(), title=e) + frappe.log_error(message=e, title='Shopify Webhooks Issue') for d in deleted_webhooks: self.remove(d) @@ -125,4 +136,3 @@ def setup_custom_fields(): } create_custom_fields(custom_fields) - diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py index bde101123d..f9f0bb3cec 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/sync_product.py @@ -8,7 +8,7 @@ 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/2019-04/products/{0}.json".format(item.get("product_id")), shopify_settings) + url = get_shopify_url("admin/api/2020-04/products/{0}.json".format(item.get("product_id")), shopify_settings) session = get_request_session() try: diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js index d84c8234ef..fd16d1e84a 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js @@ -1,7 +1,9 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Tally Migration', { +frappe.provide("erpnext.tally_migration"); + +frappe.ui.form.on("Tally Migration", { onload: function (frm) { let reload_status = true; frappe.realtime.on("tally_migration_progress_update", function (data) { @@ -35,7 +37,17 @@ frappe.ui.form.on('Tally Migration', { } }); }, + refresh: function (frm) { + frm.trigger("show_logs_preview"); + erpnext.tally_migration.failed_import_log = JSON.parse(frm.doc.failed_import_log); + erpnext.tally_migration.fixed_errors_log = JSON.parse(frm.doc.fixed_errors_log); + + ["default_round_off_account", "default_warehouse", "default_cost_center"].forEach(account => { + frm.toggle_reqd(account, frm.doc.is_master_data_imported === 1) + frm.toggle_enable(account, frm.doc.is_day_book_data_processed != 1) + }) + if (frm.doc.master_data && !frm.doc.is_master_data_imported) { if (frm.doc.is_master_data_processed) { if (frm.doc.status != "Importing Master Data") { @@ -47,6 +59,7 @@ frappe.ui.form.on('Tally Migration', { } } } + if (frm.doc.day_book_data && !frm.doc.is_day_book_data_imported) { if (frm.doc.is_day_book_data_processed) { if (frm.doc.status != "Importing Day Book Data") { @@ -59,6 +72,17 @@ frappe.ui.form.on('Tally Migration', { } } }, + + erpnext_company: function (frm) { + frappe.db.exists("Company", frm.doc.erpnext_company).then(exists => { + if (exists) { + frappe.msgprint( + __("Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts", [frm.doc.erpnext_company]), + ); + } + }); + }, + add_button: function (frm, label, method) { frm.add_custom_button( label, @@ -71,5 +95,255 @@ frappe.ui.form.on('Tally Migration', { frm.reload_doc(); } ); + }, + + render_html_table(frm, shown_logs, hidden_logs, field) { + if (shown_logs && shown_logs.length > 0) { + frm.toggle_display(field, true); + } else { + frm.toggle_display(field, false); + return + } + let rows = erpnext.tally_migration.get_html_rows(shown_logs, field); + let rows_head, table_caption; + + let table_footer = (hidden_logs && (hidden_logs.length > 0)) ? ` + And ${hidden_logs.length} more others + `: ""; + + if (field === "fixed_error_log_preview") { + rows_head = `${__("Meta Data")} + ${__("Unresolve")}` + table_caption = "Resolved Issues" + } else { + rows_head = `${__("Error Message")} + ${__("Create")}` + table_caption = "Error Log" + } + + frm.get_field(field).$wrapper.html(` + + + + + + ${rows_head} + + ${rows} + ${table_footer} +
${table_caption}
${__("#")}${__("DocType")}
+ `); + }, + + show_error_summary(frm) { + let summary = erpnext.tally_migration.failed_import_log.reduce((summary, row) => { + if (row.doc) { + if (summary[row.doc.doctype]) { + summary[row.doc.doctype] += 1; + } else { + summary[row.doc.doctype] = 1; + } + } + return summary + }, {}); + console.table(summary); + }, + + show_logs_preview(frm) { + let empty = "[]"; + let import_log = frm.doc.failed_import_log || empty; + let completed_log = frm.doc.fixed_errors_log || empty; + let render_section = !(import_log === completed_log && import_log === empty); + + frm.toggle_display("import_log_section", render_section); + if (render_section) { + frm.trigger("show_error_summary"); + frm.trigger("show_errored_import_log"); + frm.trigger("show_fixed_errors_log"); + } + }, + + show_errored_import_log(frm) { + let import_log = erpnext.tally_migration.failed_import_log; + let logs = import_log.slice(0, 20); + let hidden_logs = import_log.slice(20); + + frm.events.render_html_table(frm, logs, hidden_logs, "failed_import_preview"); + }, + + show_fixed_errors_log(frm) { + let completed_log = erpnext.tally_migration.fixed_errors_log; + let logs = completed_log.slice(0, 20); + let hidden_logs = completed_log.slice(20); + + frm.events.render_html_table(frm, logs, hidden_logs, "fixed_error_log_preview"); } }); + +erpnext.tally_migration.getError = (traceback) => { + /* Extracts the Error Message from the Python Traceback or Solved error */ + let is_multiline = traceback.trim().indexOf("\n") != -1; + let message; + + if (is_multiline) { + let exc_error_idx = traceback.trim().lastIndexOf("\n") + 1 + let error_line = traceback.substr(exc_error_idx) + let split_str_idx = (error_line.indexOf(':') > 0) ? error_line.indexOf(':') + 1 : 0; + message = error_line.slice(split_str_idx).trim(); + } else { + message = traceback; + } + + return message +} + +erpnext.tally_migration.cleanDoc = (obj) => { + /* Strips all null and empty values of your JSON object */ + let temp = obj; + $.each(temp, function(key, value){ + if (value === "" || value === null){ + delete obj[key]; + } else if (Object.prototype.toString.call(value) === '[object Object]') { + erpnext.tally_migration.cleanDoc(value); + } else if ($.isArray(value)) { + $.each(value, function (k,v) { erpnext.tally_migration.cleanDoc(v); }); + } + }); + return temp; +} + +erpnext.tally_migration.unresolve = (document) => { + /* Mark document migration as unresolved ie. move to failed error log */ + let frm = cur_frm; + let failed_log = erpnext.tally_migration.failed_import_log; + let fixed_log = erpnext.tally_migration.fixed_errors_log; + + let modified_fixed_log = fixed_log.filter(row => { + if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) { + return row + } + }); + + failed_log.push({ doc: document, exc: `Marked unresolved on ${Date()}` }); + + frm.doc.failed_import_log = JSON.stringify(failed_log); + frm.doc.fixed_errors_log = JSON.stringify(modified_fixed_log); + + frm.dirty(); + frm.save(); +} + +erpnext.tally_migration.resolve = (document) => { + /* Mark document migration as resolved ie. move to fixed error log */ + let frm = cur_frm; + let failed_log = erpnext.tally_migration.failed_import_log; + let fixed_log = erpnext.tally_migration.fixed_errors_log; + + let modified_failed_log = failed_log.filter(row => { + if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) { + return row + } + }); + fixed_log.push({ doc: document, exc: `Solved on ${Date()}` }); + + frm.doc.failed_import_log = JSON.stringify(modified_failed_log); + frm.doc.fixed_errors_log = JSON.stringify(fixed_log); + + frm.dirty(); + frm.save(); +} + +erpnext.tally_migration.create_new_doc = (document) => { + /* Mark as resolved and create new document */ + erpnext.tally_migration.resolve(document); + return frappe.call({ + type: "POST", + method: 'erpnext.erpnext_integrations.doctype.tally_migration.tally_migration.new_doc', + args: { + document + }, + freeze: true, + callback: function(r) { + if(!r.exc) { + frappe.model.sync(r.message); + frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = true; + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }); +} + +erpnext.tally_migration.get_html_rows = (logs, field) => { + let index = 0; + let rows = logs + .map(({ doc, exc }) => { + let id = frappe.dom.get_unique_id(); + let traceback = exc; + + let error_message = erpnext.tally_migration.getError(traceback); + index++; + + let show_traceback = ` + +
+
+
${traceback}
+
+
`; + + let show_doc = ` + +
+
+
${JSON.stringify(erpnext.tally_migration.cleanDoc(doc), null, 1)}
+
+
`; + + let create_button = ` + ` + + let mark_as_unresolved = ` + ` + + if (field === "fixed_error_log_preview") { + return ` + ${index} + +
${doc.doctype}
+ + +
${error_message}
+
${show_doc}
+ + +
${mark_as_unresolved}
+ + `; + } else { + return ` + ${index} + +
${doc.doctype}
+ + +
${error_message}
+
${show_traceback}
+
${show_doc}
+ + +
${create_button}
+ + `; + } + }).join(""); + + return rows +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json index dc6f093ac9..417d943792 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.json @@ -28,14 +28,19 @@ "vouchers", "accounts_section", "default_warehouse", - "round_off_account", + "default_round_off_account", "column_break_21", "default_cost_center", "day_book_section", "day_book_data", "column_break_27", "is_day_book_data_processed", - "is_day_book_data_imported" + "is_day_book_data_imported", + "import_log_section", + "failed_import_log", + "fixed_errors_log", + "failed_import_preview", + "fixed_error_log_preview" ], "fields": [ { @@ -57,6 +62,7 @@ "fieldname": "tally_creditors_account", "fieldtype": "Data", "label": "Tally Creditors Account", + "read_only_depends_on": "eval:doc.is_master_data_processed==1", "reqd": 1 }, { @@ -69,6 +75,7 @@ "fieldname": "tally_debtors_account", "fieldtype": "Data", "label": "Tally Debtors Account", + "read_only_depends_on": "eval:doc.is_master_data_processed==1", "reqd": 1 }, { @@ -92,7 +99,7 @@ "fieldname": "erpnext_company", "fieldtype": "Data", "label": "ERPNext Company", - "read_only_depends_on": "eval:doc.is_master_data_processed == 1" + "read_only_depends_on": "eval:doc.is_master_data_processed==1" }, { "fieldname": "processed_files_section", @@ -136,6 +143,7 @@ }, { "depends_on": "is_master_data_imported", + "description": "The accounts are set by the system automatically but do confirm these defaults", "fieldname": "accounts_section", "fieldtype": "Section Break", "label": "Accounts" @@ -146,12 +154,6 @@ "label": "Default Warehouse", "options": "Warehouse" }, - { - "fieldname": "round_off_account", - "fieldtype": "Link", - "label": "Round Off Account", - "options": "Account" - }, { "fieldname": "column_break_21", "fieldtype": "Column Break" @@ -212,11 +214,47 @@ "fieldname": "default_uom", "fieldtype": "Link", "label": "Default UOM", - "options": "UOM" + "options": "UOM", + "read_only_depends_on": "eval:doc.is_master_data_imported==1" + }, + { + "default": "[]", + "fieldname": "failed_import_log", + "fieldtype": "Code", + "hidden": 1, + "options": "JSON" + }, + { + "fieldname": "failed_import_preview", + "fieldtype": "HTML", + "label": "Failed Import Log" + }, + { + "fieldname": "import_log_section", + "fieldtype": "Section Break", + "label": "Import Log" + }, + { + "fieldname": "default_round_off_account", + "fieldtype": "Link", + "label": "Default Round Off Account", + "options": "Account" + }, + { + "default": "[]", + "fieldname": "fixed_errors_log", + "fieldtype": "Code", + "hidden": 1, + "options": "JSON" + }, + { + "fieldname": "fixed_error_log_preview", + "fieldtype": "HTML", + "label": "Fixed Error Log" } ], "links": [], - "modified": "2020-04-16 13:03:28.894919", + "modified": "2020-04-28 00:29:18.039826", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Tally Migration", diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index 13474e19ee..462685f5e7 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import json import re +import sys import traceback import zipfile from decimal import Decimal @@ -15,18 +16,34 @@ from bs4 import BeautifulSoup as bs import frappe from erpnext import encode_company_abbr from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts +from erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer import unset_existing_data + from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.model.document import Document from frappe.model.naming import getseries, revert_series_if_last from frappe.utils.data import format_datetime - PRIMARY_ACCOUNT = "Primary" VOUCHER_CHUNK_SIZE = 500 +@frappe.whitelist() +def new_doc(document): + document = json.loads(document) + doctype = document.pop("doctype") + document.pop("name", None) + doc = frappe.new_doc(doctype) + doc.update(document) + + return doc + class TallyMigration(Document): + def validate(self): + failed_import_log = json.loads(self.failed_import_log) + sorted_failed_import_log = sorted(failed_import_log, key=lambda row: row["doc"]["creation"]) + self.failed_import_log = json.dumps(sorted_failed_import_log) + def autoname(self): if not self.name: self.name = "Tally Migration on " + format_datetime(self.creation) @@ -65,9 +82,17 @@ class TallyMigration(Document): "attached_to_name": self.name, "content": json.dumps(value), "is_private": True - }).insert() + }) + try: + f.insert() + except frappe.DuplicateEntryError: + pass 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") + def _process_master_data(self): def get_company_name(collection): return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string.strip() @@ -84,7 +109,11 @@ class TallyMigration(Document): children, parents = get_children_and_parent_dict(accounts) group_set = [acc[1] for acc in accounts if acc[2]] children, customers, suppliers = remove_parties(parents, children, group_set) - coa = traverse({}, children, roots, roots, 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")) for account in coa: coa[account]["root_type"] = root_type_map[account] @@ -126,14 +155,18 @@ class TallyMigration(Document): def remove_parties(parents, children, group_set): customers, suppliers = set(), set() for account in parents: + found = False if self.tally_creditors_account in parents[account]: - children.pop(account, None) + found = True if account not in group_set: suppliers.add(account) - elif self.tally_debtors_account in parents[account]: - children.pop(account, None) + if self.tally_debtors_account in parents[account]: + found = True if account not in group_set: customers.add(account) + if found: + children.pop(account, None) + return children, customers, suppliers def traverse(tree, children, accounts, roots, group_set): @@ -151,6 +184,7 @@ class TallyMigration(Document): parties, addresses = [], [] for account in collection.find_all("LEDGER"): party_type = None + links = [] if account.NAME.string.strip() in customers: party_type = "Customer" parties.append({ @@ -161,7 +195,9 @@ class TallyMigration(Document): "territory": "All Territories", "customer_type": "Individual", }) - elif account.NAME.string.strip() in suppliers: + 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, @@ -170,6 +206,8 @@ class TallyMigration(Document): "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({ @@ -183,7 +221,7 @@ class TallyMigration(Document): "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": [{"link_doctype": party_type, "link_name": account["NAME"]}], + "links": links }) return parties, addresses @@ -242,12 +280,18 @@ class TallyMigration(Document): def create_company_and_coa(coa_file_url): coa_file = frappe.get_doc("File", {"file_url": coa_file_url}) frappe.local.flags.ignore_chart_of_accounts = True - company = frappe.get_doc({ - "doctype": "Company", - "company_name": self.erpnext_company, - "default_currency": "INR", - "enable_perpetual_inventory": 0, - }).insert() + + try: + 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) + frappe.local.flags.ignore_chart_of_accounts = False create_charts(company.name, custom_chart=json.loads(coa_file.get_content())) company.create_default_warehouses() @@ -256,36 +300,35 @@ class TallyMigration(Document): parties_file = frappe.get_doc("File", {"file_url": parties_file_url}) for party in json.loads(parties_file.get_content()): try: - frappe.get_doc(party).insert() + party_doc = frappe.get_doc(party) + party_doc.insert() except: - self.log(party) + self.log(party_doc) addresses_file = frappe.get_doc("File", {"file_url": addresses_file_url}) for address in json.loads(addresses_file.get_content()): try: - frappe.get_doc(address).insert(ignore_mandatory=True) + address_doc = frappe.get_doc(address) + address_doc.insert(ignore_mandatory=True) except: - try: - gstin = address.pop("gstin", None) - frappe.get_doc(address).insert(ignore_mandatory=True) - self.log({"address": address, "message": "Invalid GSTIN: {}. Address was created without GSTIN".format(gstin)}) - except: - self.log(address) + self.log(address_doc) def create_items_uoms(items_file_url, uoms_file_url): uoms_file = frappe.get_doc("File", {"file_url": uoms_file_url}) for uom in json.loads(uoms_file.get_content()): if not frappe.db.exists(uom): try: - frappe.get_doc(uom).insert() + uom_doc = frappe.get_doc(uom) + uom_doc.insert() except: - self.log(uom) + self.log(uom_doc) items_file = frappe.get_doc("File", {"file_url": items_file_url}) for item in json.loads(items_file.get_content()): try: - frappe.get_doc(item).insert() + item_doc = frappe.get_doc(item) + item_doc.insert() except: - self.log(item) + self.log(item_doc) try: self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4) @@ -299,10 +342,13 @@ class TallyMigration(Document): self.publish("Import Master Data", _("Done"), 4, 4) + self.set_account_defaults() self.is_master_data_imported = 1 + frappe.db.commit() except: self.publish("Import Master Data", _("Process Failed"), -1, 5) + frappe.db.rollback() self.log() finally: @@ -323,7 +369,9 @@ class TallyMigration(Document): processed_voucher = function(voucher) if processed_voucher: vouchers.append(processed_voucher) + frappe.db.commit() except: + frappe.db.rollback() self.log(voucher) return vouchers @@ -349,6 +397,7 @@ class TallyMigration(Document): journal_entry = { "doctype": "Journal Entry", "tally_guid": voucher.GUID.string.strip(), + "tally_voucher_no": voucher.VOUCHERNUMBER.string.strip() if voucher.VOUCHERNUMBER else "", "posting_date": voucher.DATE.string.strip(), "company": self.erpnext_company, "accounts": accounts, @@ -377,6 +426,7 @@ class TallyMigration(Document): "doctype": doctype, party_field: voucher.PARTYNAME.string.strip(), "tally_guid": voucher.GUID.string.strip(), + "tally_voucher_no": voucher.VOUCHERNUMBER.string.strip() if voucher.VOUCHERNUMBER else "", "posting_date": voucher.DATE.string.strip(), "due_date": voucher.DATE.string.strip(), "items": get_voucher_items(voucher, doctype), @@ -468,14 +518,21 @@ class TallyMigration(Document): oldest_year = new_year def create_custom_fields(doctypes): - for doctype in doctypes: - df = { - "fieldtype": "Data", - "fieldname": "tally_guid", - "read_only": 1, - "label": "Tally GUID" - } - create_custom_field(doctype, df) + tally_guid_df = { + "fieldtype": "Data", + "fieldname": "tally_guid", + "read_only": 1, + "label": "Tally GUID" + } + tally_voucher_no_df = { + "fieldtype": "Data", + "fieldname": "tally_voucher_no", + "read_only": 1, + "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({ @@ -490,7 +547,7 @@ class TallyMigration(Document): 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.round_off_account) + 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()) @@ -521,11 +578,14 @@ class TallyMigration(Document): for index, voucher in enumerate(chunk, start=start): try: - doc = frappe.get_doc(voucher).insert() - doc.submit() + voucher_doc = frappe.get_doc(voucher) + voucher_doc.insert() + voucher_doc.submit() self.publish("Importing Vouchers", _("{} of {}").format(index, total), index, total) + frappe.db.commit() except: - self.log(voucher) + frappe.db.rollback() + self.log(voucher_doc) if is_last: self.status = "" @@ -551,9 +611,22 @@ class TallyMigration(Document): frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600) def log(self, data=None): - data = data or self.status - 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) + if isinstance(data, frappe.model.document.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=(',', ':')) + 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()]) + return frappe.log_error(title="Tally Migration Error", message=message) def set_status(self, status=""): self.status = status diff --git a/erpnext/hr/doctype/employee_benefit_application/__init__.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_benefit_application/__init__.py rename to erpnext/erpnext_integrations/doctype/taxjar_settings/__init__.py diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js new file mode 100644 index 0000000000..62d5709f51 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js @@ -0,0 +1,9 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('TaxJar Settings', { + is_sandbox: (frm) => { + frm.toggle_reqd("api_key", !frm.doc.is_sandbox); + frm.toggle_reqd("sandbox_api_key", frm.doc.is_sandbox); + } +}); diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json new file mode 100644 index 0000000000..c0d60f7a31 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json @@ -0,0 +1,110 @@ +{ + "actions": [], + "creation": "2017-06-15 08:21:24.624315", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "is_sandbox", + "taxjar_calculate_tax", + "taxjar_create_transactions", + "credentials", + "api_key", + "cb_keys", + "sandbox_api_key", + "configuration", + "tax_account_head", + "configuration_cb", + "shipping_account_head" + ], + "fields": [ + { + "fieldname": "credentials", + "fieldtype": "Section Break", + "label": "Credentials" + }, + { + "fieldname": "api_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Live API Key", + "reqd": 1 + }, + { + "fieldname": "configuration", + "fieldtype": "Section Break", + "label": "Configuration" + }, + { + "fieldname": "tax_account_head", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Tax Account Head", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "shipping_account_head", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Shipping Account Head", + "options": "Account", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "is_sandbox", + "fieldtype": "Check", + "label": "Sandbox Mode" + }, + { + "fieldname": "sandbox_api_key", + "fieldtype": "Password", + "label": "Sandbox API Key" + }, + { + "fieldname": "configuration_cb", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "taxjar_create_transactions", + "fieldtype": "Check", + "label": "Create TaxJar Transaction" + }, + { + "default": "0", + "fieldname": "taxjar_calculate_tax", + "fieldtype": "Check", + "label": "Enable Tax Calculation" + }, + { + "fieldname": "cb_keys", + "fieldtype": "Column Break" + } + ], + "issingle": 1, + "links": [], + "modified": "2020-04-30 04:38:03.311089", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "TaxJar Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py new file mode 100644 index 0000000000..7f5f0f0e7a --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class TaxJarSettings(Document): + pass diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/test_taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/test_taxjar_settings.py new file mode 100644 index 0000000000..7cdfd00956 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/test_taxjar_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestTaxJarSettings(unittest.TestCase): + pass diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py new file mode 100644 index 0000000000..633692dd24 --- /dev/null +++ b/erpnext/erpnext_integrations/taxjar_integration.py @@ -0,0 +1,251 @@ +import traceback + +import pycountry +import taxjar + +import frappe +from erpnext import get_default_company +from frappe import _ +from frappe.contacts.doctype.address.address import get_company_address + +TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head") +SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head") +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") +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"] + + +def get_client(): + taxjar_settings = frappe.get_single("TaxJar Settings") + + if not taxjar_settings.is_sandbox: + api_key = taxjar_settings.api_key and taxjar_settings.get_password("api_key") + api_url = taxjar.DEFAULT_API_URL + else: + api_key = taxjar_settings.sandbox_api_key and taxjar_settings.get_password("sandbox_api_key") + api_url = taxjar.SANDBOX_API_URL + + if api_key and api_url: + return taxjar.Client(api_key=api_key, api_url=api_url) + + +def create_transaction(doc, method): + """Create an order transaction in TaxJar""" + + if not TAXJAR_CREATE_TRANSACTIONS: + return + + client = get_client() + + if not client: + return + + sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD]) + + if not sales_tax: + return + + tax_dict = get_tax_data(doc) + + 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'] + + try: + client.create_order(tax_dict) + except taxjar.exceptions.TaxJarResponseError as err: + frappe.throw(_(sanitize_error_response(err))) + except Exception as ex: + print(traceback.format_exc(ex)) + + +def delete_transaction(doc, method): + """Delete an existing TaxJar order transaction""" + + if not TAXJAR_CREATE_TRANSACTIONS: + return + + client = get_client() + + if not client: + return + + client.delete_order(doc.name) + + +def get_tax_data(doc): + from_address = get_company_address_details(doc) + from_shipping_state = from_address.get("state") + from_country_code = frappe.db.get_value("Country", from_address.country, "code") + from_country_code = from_country_code.upper() + + to_address = get_shipping_address_details(doc) + to_shipping_state = to_address.get("state") + to_country_code = frappe.db.get_value("Country", to_address.country, "code") + to_country_code = to_country_code.upper() + + if to_country_code not in SUPPORTED_COUNTRY_CODES: + return + + shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD]) + + if to_shipping_state is not None: + to_shipping_state = get_iso_3166_2_state_code(to_address) + + tax_dict = { + 'from_country': from_country_code, + 'from_zip': from_address.pincode, + 'from_state': from_shipping_state, + 'from_city': from_address.city, + 'from_street': from_address.address_line1, + 'to_country': to_country_code, + 'to_zip': to_address.pincode, + 'to_city': to_address.city, + 'to_street': to_address.address_line1, + 'to_state': to_shipping_state, + 'shipping': shipping, + 'amount': doc.net_total + } + + return tax_dict + + +def set_sales_tax(doc, method): + if not TAXJAR_CALCULATE_TAX: + return + + if not doc.items: + return + + # if the party is exempt from sales tax, then set all tax account heads to zero + 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: + if tax.account_head == TAX_ACCOUNT_HEAD: + tax.tax_amount = 0 + break + + doc.run_method("calculate_taxes_and_totals") + return + + tax_dict = get_tax_data(doc) + + if not tax_dict: + # Remove existing tax rows if address is changed from a taxable state/country + setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD]) + return + + tax_data = validate_tax_request(tax_dict) + + if tax_data is not None: + if not tax_data.amount_to_collect: + setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD]) + elif tax_data.amount_to_collect > 0: + # Loop through tax rows for existing Sales Tax entry + # If none are found, add a row with the tax amount + for tax in doc.taxes: + if tax.account_head == TAX_ACCOUNT_HEAD: + tax.tax_amount = tax_data.amount_to_collect + + 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.run_method("calculate_taxes_and_totals") + + +def validate_tax_request(tax_dict): + """Return the sales tax that should be collected for a given order.""" + + client = get_client() + + if not client: + return + + try: + tax_data = client.tax_for_order(tax_dict) + except taxjar.exceptions.TaxJarResponseError as err: + frappe.throw(_(sanitize_error_response(err))) + else: + return tax_data + + +def get_company_address_details(doc): + """Return default company address details""" + + company_address = get_company_address(get_default_company()).company_address + + if not company_address: + frappe.throw(_("Please set a default company address")) + + company_address = frappe.get_doc("Address", company_address) + return company_address + + +def get_shipping_address_details(doc): + """Return customer shipping address details""" + + if doc.shipping_address_name: + shipping_address = frappe.get_doc("Address", doc.shipping_address_name) + else: + shipping_address = get_company_address_details(doc) + + return shipping_address + + +def get_iso_3166_2_state_code(address): + 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")) + state = address.get("state").upper().strip() + + # The max length for ISO state codes is 3, excluding the country code + if len(state) <= 3: + # PyCountry returns state code as {country_code}-{state-code} (e.g. US-FL) + address_state = (country_code + "-" + state).upper() + + states = pycountry.subdivisions.get(country_code=country_code.upper()) + states = [pystate.code for pystate in states] + + if address_state in states: + return state + + frappe.throw(_(error_message)) + else: + try: + lookup_state = pycountry.subdivisions.lookup(state) + except LookupError: + frappe.throw(_(error_message)) + else: + return lookup_state.code.split('-')[1] + + +def sanitize_error_response(response): + response = response.full_response.get("detail") + response = response.replace("_", " ") + + sanitized_responses = { + "to zip": "Zipcode", + "to city": "City", + "to state": "State", + "to country": "Country" + } + + for k, v in sanitized_responses.items(): + response = response.replace(k, v) + + return response diff --git a/erpnext/hr/doctype/employee_benefit_application_detail/__init__.py b/erpnext/healthcare/dashboard_chart_source/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_benefit_application_detail/__init__.py rename to erpnext/healthcare/dashboard_chart_source/__init__.py diff --git a/erpnext/hr/doctype/employee_benefit_claim/__init__.py b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_benefit_claim/__init__.py rename to erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/__init__.py diff --git a/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.js b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.js new file mode 100644 index 0000000000..dd6dc666d2 --- /dev/null +++ b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.js @@ -0,0 +1,14 @@ +frappe.provide('frappe.dashboards.chart_sources'); + +frappe.dashboards.chart_sources["Department wise Patient Appointments"] = { + method: "erpnext.healthcare.dashboard_chart_source.department_wise_patient_appointments.department_wise_patient_appointments.get", + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company") + } + ] +}; \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.json b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.json new file mode 100644 index 0000000000..00301ef2c3 --- /dev/null +++ b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.json @@ -0,0 +1,13 @@ +{ + "creation": "2020-05-18 19:18:42.571045", + "docstatus": 0, + "doctype": "Dashboard Chart Source", + "idx": 0, + "modified": "2020-05-18 19:18:42.571045", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Department wise Patient Appointments", + "owner": "Administrator", + "source_name": "Department wise Patient Appointments", + "timeseries": 0 +} \ No newline at end of file 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 new file mode 100644 index 0000000000..062da6e465 --- /dev/null +++ b/erpnext/healthcare/dashboard_chart_source/department_wise_patient_appointments/department_wise_patient_appointments.py @@ -0,0 +1,72 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +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): + if 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']) + if not filters: + filters = {} + + status = ['Open', 'Scheduled', 'Closed', 'Cancelled'] + for department in data: + 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') + + 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] + + labels = [] + open_appointments = [] + scheduled = [] + closed = [] + cancelled = [] + + for department in sorted_department_map: + labels.append(department.name) + open_appointments.append(department.open) + scheduled.append(department.scheduled) + closed.append(department.closed) + 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 + } + ], + 'type': 'bar' + } \ No newline at end of file diff --git a/erpnext/healthcare/dashboard_fixtures.py b/erpnext/healthcare/dashboard_fixtures.py index fc3d62f2d8..94668a16d9 100644 --- a/erpnext/healthcare/dashboard_fixtures.py +++ b/erpnext/healthcare/dashboard_fixtures.py @@ -3,33 +3,60 @@ import frappe import json - +from frappe import _ def get_data(): return frappe._dict({ "dashboards": get_dashboards(), "charts": get_charts(), + "number_cards": get_number_cards(), }) +def get_company(): + company = frappe.defaults.get_defaults().company + if company: + return company + else: + company = frappe.get_list("Company", limit=1) + if company: + return company[0].name + return None + def get_dashboards(): return [{ "name": "Healthcare", "dashboard_name": "Healthcare", "charts": [ - { "chart": "Patient Appointments" } + { "chart": "Patient Appointments", "width": "Full"}, + { "chart": "In-Patient Status", "width": "Half"}, + { "chart": "Clinical Procedures Status", "width": "Half"}, + { "chart": "Lab Tests", "width": "Half"}, + { "chart": "Clinical Procedures", "width": "Half"}, + { "chart": "Symptoms", "width": "Half"}, + { "chart": "Diagnoses", "width": "Half"}, + { "chart": "Department wise Patient Appointments", "width": "Full"} + ], + "cards": [ + { "card": "Total Patients" }, + { "card": "Total Patient Admitted" }, + { "card": "Open Appointments" }, + { "card": "Appointments to Bill" } ] }] def get_charts(): + company = get_company() return [ { "doctype": "Dashboard Chart", "time_interval": "Daily", "name": "Patient Appointments", - "chart_name": "Patient Appointments", + "chart_name": _("Patient Appointments"), "timespan": "Last Month", - "color": "#77ecca", - "filters_json": json.dumps({}), + "filters_json": json.dumps([ + ["Patient Appointment", "company", "=", company, False], + ["Patient Appointment", "status", "!=", "Cancelled"] + ]), "chart_type": "Count", "timeseries": 1, "based_on": "appointment_datetime", @@ -37,5 +64,182 @@ def get_charts(): "document_type": "Patient Appointment", "type": "Line", "width": "Half" + }, + { + "doctype": "Dashboard Chart", + "name": "Department wise Patient Appointments", + "chart_name": _("Department wise Patient Appointments"), + "chart_type": "Custom", + "source": "Department wise Patient Appointments", + "filters_json": json.dumps([]), + 'is_public': 1, + "owner": "Administrator", + "type": "Bar", + "width": "Full", + "custom_options": json.dumps({ + "colors": ["#7CD5FA", "#5F62F6", "#7544E2", "#EE5555"], + "barOptions":{ + "stacked":1 + }, + "height": 300 + }) + }, + { + "doctype": "Dashboard Chart", + "name": "Lab Tests", + "chart_name": _("Lab Tests"), + "chart_type": "Group By", + "document_type": "Lab Test", + "group_by_type": "Count", + "group_by_based_on": "template", + "filters_json": json.dumps([ + ["Lab Test", "company", "=", company, False], + ["Lab Test", "docstatus", "=", 1] + ]), + 'is_public': 1, + "owner": "Administrator", + "type": "Percentage", + "width": "Half", + }, + { + "doctype": "Dashboard Chart", + "name": "Clinical Procedures", + "chart_name": _("Clinical Procedures"), + "chart_type": "Group By", + "document_type": "Clinical Procedure", + "group_by_type": "Count", + "group_by_based_on": "procedure_template", + "filters_json": json.dumps([ + ["Clinical Procedure", "company", "=", company, False], + ["Clinical Procedure", "docstatus", "=", 1] + ]), + 'is_public': 1, + "owner": "Administrator", + "type": "Percentage", + "width": "Half", + }, + { + "doctype": "Dashboard Chart", + "name": "In-Patient Status", + "chart_name": _("In-Patient Status"), + "chart_type": "Group By", + "document_type": "Inpatient Record", + "group_by_type": "Count", + "group_by_based_on": "status", + "filters_json": json.dumps([ + ["Inpatient Record", "company", "=", company, False] + ]), + 'is_public': 1, + "owner": "Administrator", + "type": "Bar", + "width": "Half", + }, + { + "doctype": "Dashboard Chart", + "name": "Clinical Procedures Status", + "chart_name": _("Clinical Procedure Status"), + "chart_type": "Group By", + "document_type": "Clinical Procedure", + "group_by_type": "Count", + "group_by_based_on": "status", + "filters_json": json.dumps([ + ["Clinical Procedure", "company", "=", company, False], + ["Clinical Procedure", "docstatus", "=", 1] + ]), + 'is_public': 1, + "owner": "Administrator", + "type": "Pie", + "width": "Half", + }, + { + "doctype": "Dashboard Chart", + "name": "Symptoms", + "chart_name": _("Symptoms"), + "chart_type": "Group By", + "document_type": "Patient Encounter Symptom", + "group_by_type": "Count", + "group_by_based_on": "complaint", + "filters_json": json.dumps([]), + 'is_public': 1, + "owner": "Administrator", + "type": "Percentage", + "width": "Half", + }, + { + "doctype": "Dashboard Chart", + "name": "Diagnoses", + "chart_name": _("Diagnoses"), + "chart_type": "Group By", + "document_type": "Patient Encounter Diagnosis", + "group_by_type": "Count", + "group_by_based_on": "diagnosis", + "filters_json": json.dumps([]), + 'is_public': 1, + "owner": "Administrator", + "type": "Percentage", + "width": "Half", } ] + +def get_number_cards(): + company = get_company() + return [ + { + "name": "Total Patients", + "label": _("Total Patients"), + "function": "Count", + "doctype": "Number Card", + "document_type": "Patient", + "filters_json": json.dumps( + [["Patient","status","=","Active",False]] + ), + "is_public": 1, + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Daily" + }, + { + "name": "Total Patients Admitted", + "label": _("Total Patients Admitted"), + "function": "Count", + "doctype": "Number Card", + "document_type": "Patient", + "filters_json": json.dumps( + [["Patient","inpatient_status","=","Admitted",False]] + ), + "is_public": 1, + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Daily" + }, + { + "name": "Open Appointments", + "label": _("Open Appointments"), + "function": "Count", + "doctype": "Number Card", + "document_type": "Patient Appointment", + "filters_json": json.dumps( + [["Patient Appointment","company","=",company,False], + ["Patient Appointment","status","=","Open",False]] + ), + "is_public": 1, + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Daily" + }, + { + "name": "Appointments to Bill", + "label": _("Appointments To Bill"), + "function": "Count", + "doctype": "Number Card", + "document_type": "Patient Appointment", + "filters_json": json.dumps( + [["Patient Appointment","company","=",company,False], + ["Patient Appointment","invoiced","=",0,False]] + ), + "is_public": 1, + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Daily" + } + ] \ No newline at end of file diff --git a/erpnext/healthcare/desk_page/healthcare/healthcare.json b/erpnext/healthcare/desk_page/healthcare/healthcare.json index 5cf09b34b2..334b65563b 100644 --- a/erpnext/healthcare/desk_page/healthcare/healthcare.json +++ b/erpnext/healthcare/desk_page/healthcare/healthcare.json @@ -60,26 +60,30 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Healthcare", - "modified": "2020-04-25 22:31:36.576444", + "modified": "2020-05-28 19:02:28.824995", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", + "onboarding": "Healthcare", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "restrict_to_domain": "Healthcare", "shortcuts": [ { + "color": "#ffe8cd", "format": "{} Open", "label": "Patient Appointment", "link_to": "Patient Appointment", - "stats_filter": "{\n \"status\": \"Open\"\n}", + "stats_filter": "{\n \"status\": \"Open\",\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%']\n}", "type": "DocType" }, { + "color": "#ffe8cd", "format": "{} Active", "label": "Patient", "link_to": "Patient", @@ -87,10 +91,11 @@ "type": "DocType" }, { + "color": "#cef6d1", "format": "{} Vacant", "label": "Healthcare Service Unit", "link_to": "Healthcare Service Unit", - "stats_filter": "{\n \"occupancy_status\": \"Vacant\",\n \"is_group\": 0\n}", + "stats_filter": "{\n \"occupancy_status\": \"Vacant\",\n \"is_group\": 0,\n \"company\": [\"like\", \"%\" + frappe.defaults.get_global_default(\"company\") + \"%\"]\n}", "type": "DocType" }, { @@ -102,6 +107,11 @@ "label": "Patient History", "link_to": "patient_history", "type": "Page" + }, + { + "label": "Dashboard", + "link_to": "Healthcare", + "type": "Dashboard" } ] } \ No newline at end of file diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.json b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.json index eaf8d80ba8..b1d62da032 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.json +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.json @@ -11,6 +11,7 @@ "title", "appointment", "procedure_template", + "medical_code", "column_break_30", "company", "invoiced", @@ -290,11 +291,19 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fetch_from": "procedure_template.medical_code", + "fieldname": "medical_code", + "fieldtype": "Link", + "label": "Medical Code", + "options": "Medical Code", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-04-27 21:36:23.796924", + "modified": "2020-06-29 14:28:11.779815", "modified_by": "Administrator", "module": "Healthcare", "name": "Clinical Procedure", diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.js b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.js index 57f4cdf3b2..1ef110dc6f 100644 --- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.js +++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.js @@ -30,6 +30,16 @@ frappe.ui.form.on('Clinical Procedure Template', { mark_change_in_item(frm); }, + medical_code: function(frm) { + frm.set_query("medical_code", function() { + return { + filters: { + medical_code_standard: frm.doc.medical_code_standard + } + }; + }); + }, + refresh: function(frm) { frm.fields_dict['items'].grid.set_column_disp('barcode', false); frm.fields_dict['items'].grid.set_column_disp('batch_no', false); @@ -144,3 +154,38 @@ cur_frm.set_query('item_code', 'items', function() { } }; }); + +frappe.tour['Clinical Procedure Template'] = [ + { + fieldname: 'template', + title: __('Template Name'), + description: __('Enter a name for the Clinical Procedure Template') + }, + { + fieldname: 'item_code', + title: __('Item Code'), + description: __('Set the Item Code which will be used for billing the Clinical Procedure.') + }, + { + fieldname: 'item_group', + title: __('Item Group'), + description: __('Select an Item Group for the Clinical Procedure Item.') + }, + { + fieldname: 'is_billable', + title: __('Clinical Procedure Rate'), + description: __('Check this if the Clinical Procedure is billable and also set the rate.') + }, + { + fieldname: 'consume_stock', + title: __('Allow Stock Consumption'), + description: __('Check this if the Clinical Procedure utilises consumables. Click ') + "here" + __(' to know more') + + }, + { + fieldname: 'medical_department', + title: __('Medical Department'), + description: __('You can also set the Medical Department for the template. After saving the document, an Item will automatically be created for billing this Clinical Procedure. You can then use this template while creating Clinical Procedures for Patients. Templates save you from filling up redundant data every single time. You can also create templates for other operations like Lab Tests, Therapy Sessions, etc.') + } +]; + diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.json b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.json index 9cfd682f1d..17ac7eb1f9 100644 --- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.json +++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.json @@ -21,6 +21,9 @@ "is_billable", "rate", "medical_department", + "medical_coding_section", + "medical_code_standard", + "medical_code", "consumables", "consume_stock", "items", @@ -46,7 +49,6 @@ "fieldname": "item_code", "fieldtype": "Data", "label": "Item Code", - "options": "Item", "read_only_depends_on": "eval: !doc.__islocal ", "reqd": 1 }, @@ -173,10 +175,29 @@ "no_copy": 1, "options": "Item", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "medical_coding_section", + "fieldtype": "Section Break", + "label": "Medical Coding" + }, + { + "fieldname": "medical_code_standard", + "fieldtype": "Link", + "label": "Medical Code Standard", + "options": "Medical Code Standard" + }, + { + "depends_on": "medical_code_standard", + "fieldname": "medical_code", + "fieldtype": "Link", + "label": "Medical Code", + "options": "Medical Code" } ], "links": [], - "modified": "2020-02-28 14:16:13.184981", + "modified": "2020-06-29 14:12:27.158130", "modified_by": "Administrator", "module": "Healthcare", "name": "Clinical Procedure Template", diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.js b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.js index 4ab3b6e9c1..fc0b24122a 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.js +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.js @@ -108,3 +108,38 @@ frappe.ui.form.on('Healthcare Practitioner', 'employee', function(frm) { }); } }); + +frappe.tour['Healthcare Practitioner'] = [ + { + fieldname: 'employee', + title: __('Employee'), + description: __('If you want to track Payroll and other HRMS operations for a Practitoner, create an Employee and link it here.') + }, + { + fieldname: 'practitioner_schedules', + title: __('Practitioner Schedules'), + description: __('Set the Practitioner Schedule you just created. This will be used while booking appointments.') + }, + { + fieldname: 'op_consulting_charge_item', + title: __('Out Patient Consulting Charge Item'), + description: __('Create a service item for Out Patient Consulting.') + }, + { + fieldname: 'inpatient_visit_charge_item', + title: __('Inpatient Visit Charge Item'), + description: __('If this Healthcare Practitioner works for the In-Patient Department, create a service item for Inpatient Visits.') + }, + { + fieldname: 'op_consulting_charge', + title: __('Out Patient Consulting Charge'), + description: __('Set the Out Patient Consulting Charge for this Practitioner.') + + }, + { + fieldname: 'inpatient_visit_charge', + title: __('Inpatient Visit Charge'), + description: __('If this Healthcare Practitioner also works for the In-Patient Department, set the inpatient visit charge for this Practitioner.') + } +]; + diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py index 0c13b6af9d..3dc7c1ec39 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py @@ -70,6 +70,7 @@ def validate_service_item(item, msg): if frappe.db.get_value('Item', item, 'is_stock_item'): frappe.throw(_(msg)) +@frappe.whitelist() def get_practitioner_list(doctype, txt, searchfield, start, page_len, filters=None): fields = ['name', 'practitioner_name', 'mobile_phone'] 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 ea4ae846f7..9ee865a62a 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json @@ -12,7 +12,6 @@ "engine": "InnoDB", "field_order": [ "healthcare_service_unit_name", - "parent_healthcare_service_unit", "is_group", "service_unit_type", "allow_appointments", @@ -20,8 +19,10 @@ "inpatient_occupancy", "occupancy_status", "column_break_9", - "warehouse", "company", + "warehouse", + "tree_details_section", + "parent_healthcare_service_unit", "lft", "rgt", "old_parent" @@ -51,7 +52,6 @@ "depends_on": "eval:doc.inpatient_occupancy != 1 && doc.allow_appointments != 1", "fieldname": "is_group", "fieldtype": "Check", - "in_list_view": 1, "label": "Is Group" }, { @@ -63,12 +63,12 @@ "options": "Healthcare Service Unit Type" }, { - "bold": 1, "default": "0", "depends_on": "eval:doc.is_group != 1 && doc.inpatient_occupancy != 1", "fetch_from": "service_unit_type.allow_appointments", "fieldname": "allow_appointments", "fieldtype": "Check", + "in_list_view": 1, "label": "Allow Appointments", "no_copy": 1, "read_only": 1 @@ -90,6 +90,7 @@ "fetch_from": "service_unit_type.inpatient_occupancy", "fieldname": "inpatient_occupancy", "fieldtype": "Check", + "in_list_view": 1, "label": "Inpatient Occupancy", "no_copy": 1, "read_only": 1, @@ -101,7 +102,7 @@ "fieldtype": "Select", "label": "Occupancy Status", "no_copy": 1, - "options": "\nVacant\nOccupied", + "options": "Vacant\nOccupied", "read_only": 1 }, { @@ -157,10 +158,16 @@ "options": "Healthcare Service Unit", "print_hide": 1, "report_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "tree_details_section", + "fieldtype": "Section Break", + "label": "Tree Details" } ], "links": [], - "modified": "2020-03-26 16:13:08.675952", + "modified": "2020-05-20 18:26:56.065543", "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 13cc43d2be..9e0417a2be 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py @@ -22,10 +22,16 @@ class HealthcareServiceUnit(NestedSet): super(HealthcareServiceUnit, self).on_update() self.validate_one_root() - def validate(self): + def after_insert(self): if self.is_group: self.allow_appointments = 0 self.overlap_appointments = 0 self.inpatient_occupancy = 0 - elif not self.allow_appointments: - self.overlap_appointments = 0 + 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.overlap_appointments = service_unit_type.overlap_appointments + self.inpatient_occupancy = service_unit_type.inpatient_occupancy + if self.inpatient_occupancy: + self.occupancy_status = 'Vacant' + self.overlap_appointments = 0 diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json index 5fa47d91bc..4b8503d028 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json +++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.json @@ -31,6 +31,7 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Service Unit Type", + "no_copy": 1, "reqd": 1, "unique": 1 }, @@ -40,8 +41,7 @@ "depends_on": "eval:doc.inpatient_occupancy != 1", "fieldname": "allow_appointments", "fieldtype": "Check", - "label": "Allow Appointments", - "no_copy": 1 + "label": "Allow Appointments" }, { "bold": 1, @@ -49,8 +49,7 @@ "depends_on": "eval:doc.allow_appointments == 1 && doc.inpatient_occupany != 1", "fieldname": "overlap_appointments", "fieldtype": "Check", - "label": "Allow Overlap", - "no_copy": 1 + "label": "Allow Overlap" }, { "bold": 1, @@ -58,8 +57,7 @@ "depends_on": "eval:doc.allow_appointments != 1", "fieldname": "inpatient_occupancy", "fieldtype": "Check", - "label": "Inpatient Occupancy", - "no_copy": 1 + "label": "Inpatient Occupancy" }, { "bold": 1, @@ -79,6 +77,7 @@ "fieldname": "item", "fieldtype": "Link", "label": "Item", + "no_copy": 1, "options": "Item", "read_only": 1 }, @@ -86,7 +85,8 @@ "fieldname": "item_code", "fieldtype": "Data", "label": "Item Code", - "mandatory_depends_on": "eval: doc.is_billable == 1" + "mandatory_depends_on": "eval: doc.is_billable == 1", + "no_copy": 1 }, { "fieldname": "item_group", @@ -138,7 +138,7 @@ } ], "links": [], - "modified": "2020-01-30 16:06:00.624496", + "modified": "2020-05-20 15:31:09.627516", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Service Unit Type", 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 286ecc0ff8..bb86eaacc4 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 @@ -10,6 +10,22 @@ from frappe.model.rename_doc import rename_doc 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' + ) + 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' + ) + + if not self.allow_appointments: + self.overlap_appointments = 0 + if self.is_billable: if self.disabled: frappe.db.set_value('Item', self.item, 'disabled', 1) diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.js b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.js index 22fbf5019a..cf2276fc07 100644 --- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.js +++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.js @@ -39,3 +39,37 @@ var set_query_service_item = function(frm, service_item_field) { }; }); }; + +frappe.tour['Healthcare Settings'] = [ + { + fieldname: 'link_customer_to_patient', + title: __('Link Customer to Patient'), + description: __('If checked, a customer will be created for every Patient. Patient Invoices will be created against this Customer. You can also select existing Customer while creating a Patient. This field is checked by default.') + }, + { + fieldname: 'collect_registration_fee', + title: __('Collect Registration Fee'), + description: __('If your Healthcare facility bills registrations of Patients, you can check this and set the Registration Fee in the field below. Checking this will create new Patients with a Disabled status by default and will only be enabled after invoicing the Registration Fee.') + }, + { + fieldname: 'automate_appointment_invoicing', + title: __('Automate Appointment Invoicing'), + description: __('Checking this will automatically create a Sales Invoice whenever an appointment is booked for a Patient.') + }, + { + fieldname: 'inpatient_visit_charge_item', + title: __('Healthcare Service Items'), + description: __('You can create a service item for Inpatient Visit Charge and set it here. Similarly, you can set up other Healthcare Service Items for billing in this section. Click ') + "here" + __(' to know more') + }, + { + fieldname: 'income_account', + title: __('Set up default Accounts for the Healthcare Facility'), + description: __('If you wish to override default accounts settings and configure the Income and Receivable accounts for Healthcare, you can do so here.') + + }, + { + fieldname: 'send_registration_msg', + title: __('Out Patient SMS alerts'), + description: __('If you want to send SMS alert on Patient Registration, you can enable this option. Similary, you can set up Out Patient SMS alerts for other functionalities in this section. Click ') + "here" + __(' to know more') + } +]; diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js index 67c12f6c14..971e166067 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js @@ -2,22 +2,37 @@ // For license information, please see license.txt frappe.ui.form.on('Inpatient Record', { + setup: function(frm) { + frm.get_field('drug_prescription').grid.editable_fields = [ + {fieldname: 'drug_code', columns: 2}, + {fieldname: 'drug_name', columns: 2}, + {fieldname: 'dosage', columns: 2}, + {fieldname: 'period', columns: 2} + ]; + }, refresh: function(frm) { - if(!frm.doc.__islocal && frm.doc.status == "Admission Scheduled"){ + if (!frm.doc.__islocal && (frm.doc.status == 'Admission Scheduled' || frm.doc.status == 'Admitted')) { + frm.enable_save(); + } else { + frm.disable_save(); + } + + if (!frm.doc.__islocal && frm.doc.status == 'Admission Scheduled') { frm.add_custom_button(__('Admit'), function() { admit_patient_dialog(frm); } ); - frm.set_df_property("btn_transfer", "hidden", 1); } - if(!frm.doc.__islocal && frm.doc.status == "Discharge Scheduled"){ + + if (!frm.doc.__islocal && frm.doc.status == 'Discharge Scheduled') { frm.add_custom_button(__('Discharge'), function() { discharge_patient(frm); } ); - frm.set_df_property("btn_transfer", "hidden", 0); } - if(!frm.doc.__islocal && (frm.doc.status == "Discharged" || frm.doc.status == "Discharge Scheduled")){ + if (!frm.doc.__islocal && frm.doc.status != 'Admitted') { frm.disable_save(); - frm.set_df_property("btn_transfer", "hidden", 1); + frm.set_df_property('btn_transfer', 'hidden', 1); + } else { + frm.set_df_property('btn_transfer', 'hidden', 0); } }, btn_transfer: function(frm) { @@ -25,39 +40,47 @@ frappe.ui.form.on('Inpatient Record', { } }); -var discharge_patient = function(frm) { +let discharge_patient = function(frm) { frappe.call({ doc: frm.doc, - method: "discharge", + method: 'discharge', callback: function(data) { - if(!data.exc){ + if (!data.exc) { frm.reload_doc(); } }, freeze: true, - freeze_message: "Process Discharge" + freeze_message: __('Processing Inpatient Discharge') }); }; -var admit_patient_dialog = function(frm){ - var dialog = new frappe.ui.Dialog({ +let admit_patient_dialog = function(frm) { + let dialog = new frappe.ui.Dialog({ title: 'Admit Patient', width: 100, fields: [ - {fieldtype: "Link", label: "Service Unit Type", fieldname: "service_unit_type", options: "Healthcare Service Unit Type"}, - {fieldtype: "Link", label: "Service Unit", fieldname: "service_unit", options: "Healthcare Service Unit", reqd: 1}, - {fieldtype: "Datetime", label: "Admission Datetime", fieldname: "check_in", reqd: 1}, - {fieldtype: "Date", label: "Expected Discharge", fieldname: "expected_discharge"} + {fieldtype: 'Link', label: 'Service Unit Type', fieldname: 'service_unit_type', + options: 'Healthcare Service Unit Type', default: frm.doc.admission_service_unit_type + }, + {fieldtype: 'Link', label: 'Service Unit', fieldname: 'service_unit', + options: 'Healthcare Service Unit', reqd: 1 + }, + {fieldtype: 'Datetime', label: 'Admission Datetime', fieldname: 'check_in', + reqd: 1, default: frappe.datetime.now_datetime() + }, + {fieldtype: 'Date', label: 'Expected Discharge', fieldname: 'expected_discharge', + default: frm.doc.expected_length_of_stay ? frappe.datetime.add_days(frappe.datetime.now_datetime(), frm.doc.expected_length_of_stay) : '' + } ], - primary_action_label: __("Admit"), + primary_action_label: __('Admit'), primary_action : function(){ - var service_unit = dialog.get_value('service_unit'); - var check_in = dialog.get_value('check_in'); - var expected_discharge = null; - if(dialog.get_value('expected_discharge')){ + let service_unit = dialog.get_value('service_unit'); + let check_in = dialog.get_value('check_in'); + let expected_discharge = null; + if (dialog.get_value('expected_discharge')) { expected_discharge = dialog.get_value('expected_discharge'); } - if(!service_unit && !check_in){ + if (!service_unit && !check_in) { return; } frappe.call({ @@ -69,32 +92,33 @@ var admit_patient_dialog = function(frm){ 'expected_discharge': expected_discharge }, callback: function(data) { - if(!data.exc){ + if (!data.exc) { frm.reload_doc(); } }, freeze: true, - freeze_message: "Process Admission" + freeze_message: __('Processing Patient Admission') }); frm.refresh_fields(); dialog.hide(); } }); - dialog.fields_dict["service_unit_type"].get_query = function(){ + dialog.fields_dict['service_unit_type'].get_query = function() { return { filters: { - "inpatient_occupancy": 1, - "allow_appointments": 0 + 'inpatient_occupancy': 1, + 'allow_appointments': 0 } }; }; - dialog.fields_dict["service_unit"].get_query = function(){ + dialog.fields_dict['service_unit'].get_query = function() { return { filters: { - "is_group": 0, - "service_unit_type": dialog.get_value("service_unit_type"), - "occupancy_status" : "Vacant" + 'is_group': 0, + 'company': frm.doc.company, + 'service_unit_type': dialog.get_value('service_unit_type'), + 'occupancy_status' : 'Vacant' } }; }; @@ -102,21 +126,21 @@ var admit_patient_dialog = function(frm){ dialog.show(); }; -var transfer_patient_dialog = function(frm){ - var dialog = new frappe.ui.Dialog({ +let transfer_patient_dialog = function(frm) { + let dialog = new frappe.ui.Dialog({ title: 'Transfer Patient', width: 100, fields: [ - {fieldtype: "Link", label: "Leave From", fieldname: "leave_from", options: "Healthcare Service Unit", reqd: 1, read_only:1}, - {fieldtype: "Link", label: "Service Unit Type", fieldname: "service_unit_type", options: "Healthcare Service Unit Type"}, - {fieldtype: "Link", label: "Transfer To", fieldname: "service_unit", options: "Healthcare Service Unit", reqd: 1}, - {fieldtype: "Datetime", label: "Check In", fieldname: "check_in", reqd: 1} + {fieldtype: 'Link', label: 'Leave From', fieldname: 'leave_from', options: 'Healthcare Service Unit', reqd: 1, read_only:1}, + {fieldtype: 'Link', label: 'Service Unit Type', fieldname: 'service_unit_type', options: 'Healthcare Service Unit Type'}, + {fieldtype: 'Link', label: 'Transfer To', fieldname: 'service_unit', options: 'Healthcare Service Unit', reqd: 1}, + {fieldtype: 'Datetime', label: 'Check In', fieldname: 'check_in', reqd: 1} ], - primary_action_label: __("Transfer"), - primary_action : function(){ - var service_unit = null; - var check_in = dialog.get_value('check_in'); - var leave_from = null; + primary_action_label: __('Transfer'), + primary_action : function() { + let service_unit = null; + let check_in = dialog.get_value('check_in'); + let leave_from = null; if(dialog.get_value('leave_from')){ leave_from = dialog.get_value('leave_from'); } @@ -135,47 +159,47 @@ var transfer_patient_dialog = function(frm){ 'leave_from': leave_from }, callback: function(data) { - if(!data.exc){ + if (!data.exc) { frm.reload_doc(); } }, freeze: true, - freeze_message: "Process Transfer" + freeze_message: __('Process Transfer') }); frm.refresh_fields(); dialog.hide(); } }); - dialog.fields_dict["leave_from"].get_query = function(){ + dialog.fields_dict['leave_from'].get_query = function(){ return { - query : "erpnext.healthcare.doctype.inpatient_record.inpatient_record.get_leave_from", + query : 'erpnext.healthcare.doctype.inpatient_record.inpatient_record.get_leave_from', filters: {docname:frm.doc.name} }; }; - dialog.fields_dict["service_unit_type"].get_query = function(){ + dialog.fields_dict['service_unit_type'].get_query = function(){ return { filters: { - "inpatient_occupancy": 1, - "allow_appointments": 0 + 'inpatient_occupancy': 1, + 'allow_appointments': 0 } }; }; - dialog.fields_dict["service_unit"].get_query = function(){ + dialog.fields_dict['service_unit'].get_query = function(){ return { filters: { - "is_group": 0, - "service_unit_type": dialog.get_value("service_unit_type"), - "occupancy_status" : "Vacant" + 'is_group': 0, + 'service_unit_type': dialog.get_value('service_unit_type'), + 'occupancy_status' : 'Vacant' } }; }; dialog.show(); - var not_left_service_unit = null; - for(let inpatient_occupancy in frm.doc.inpatient_occupancies){ - if(frm.doc.inpatient_occupancies[inpatient_occupancy].left != 1){ + let not_left_service_unit = null; + for (let inpatient_occupancy in frm.doc.inpatient_occupancies) { + if (frm.doc.inpatient_occupancies[inpatient_occupancy].left != 1) { not_left_service_unit = frm.doc.inpatient_occupancies[inpatient_occupancy].service_unit; } } diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json index c1b516d536..5ced845c1b 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json @@ -22,17 +22,41 @@ "scheduled_date", "admitted_datetime", "expected_discharge", - "discharge_date", "references", - "cb_admission", - "admission_practitioner", "admission_encounter", - "cb_discharge", - "discharge_practitioner", - "discharge_encounter", + "admission_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", + "chief_complaint", + "column_break_29", + "diagnosis", + "medication_section", + "drug_prescription", + "investigations_section", + "lab_test_prescription", + "procedures_section", + "procedure_prescription", + "rehabilitation_section", + "therapy_plan", + "therapies", "sb_inpatient_occupancy", "inpatient_occupancies", "btn_transfer", + "sb_discharge_details", + "discharge_ordered_date", + "discharge_practitioner", + "discharge_encounter", + "discharge_date", + "cb_discharge", + "discharge_instructions", + "followup_date", "sb_discharge_note", "discharge_note" ], @@ -54,7 +78,8 @@ "in_list_view": 1, "label": "Patient", "options": "Patient", - "reqd": 1 + "reqd": 1, + "set_only_once": 1 }, { "fetch_from": "patient.patient_name", @@ -108,11 +133,31 @@ "label": "Phone", "read_only": 1 }, + { + "fieldname": "medical_department", + "fieldtype": "Link", + "label": "Medical Department", + "options": "Medical Department", + "set_only_once": 1 + }, + { + "fieldname": "primary_practitioner", + "fieldtype": "Link", + "label": "Healthcare Practitioner (Primary)", + "options": "Healthcare Practitioner" + }, + { + "fieldname": "secondary_practitioner", + "fieldtype": "Link", + "label": "Healthcare Practitioner (Secondary)", + "options": "Healthcare Practitioner" + }, { "fieldname": "column_break_8", "fieldtype": "Column Break" }, { + "default": "Admission Scheduled", "fieldname": "status", "fieldtype": "Select", "in_list_view": 1, @@ -126,37 +171,45 @@ "fieldtype": "Date", "in_list_view": 1, "label": "Admission Schedule Date", + "read_only": 1, "reqd": 1 }, { - "default": "Today", + "fieldname": "admission_ordered_for", + "fieldtype": "Date", + "label": "Admission Ordered For", + "read_only": 1 + }, + { "fieldname": "admitted_datetime", "fieldtype": "Datetime", "in_list_view": 1, - "label": "Admitted Datetime" + "label": "Admitted Datetime", + "read_only": 1 + }, + { + "depends_on": "eval:(doc.expected_length_of_stay > 0)", + "fieldname": "expected_length_of_stay", + "fieldtype": "Int", + "label": "Expected Length of Stay", + "set_only_once": 1 }, { "fieldname": "expected_discharge", "fieldtype": "Date", "in_list_view": 1, - "label": "Expected Discharge" - }, - { - "fieldname": "discharge_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "Discharge Date" + "label": "Expected Discharge", + "read_only": 1 }, { "collapsible": 1, "fieldname": "references", "fieldtype": "Section Break", - "label": "References" + "label": "Admission Order Details" }, { "fieldname": "cb_admission", - "fieldtype": "Column Break", - "label": "Admission" + "fieldtype": "Column Break" }, { "fieldname": "admission_practitioner", @@ -172,10 +225,22 @@ "options": "Patient Encounter", "read_only": 1 }, + { + "fieldname": "chief_complaint", + "fieldtype": "Table MultiSelect", + "label": "Chief Complaint", + "options": "Patient Encounter Symptom", + "permlevel": 1 + }, + { + "fieldname": "admission_instruction", + "fieldtype": "Small Text", + "label": "Admission Instruction", + "set_only_once": 1 + }, { "fieldname": "cb_discharge", - "fieldtype": "Column Break", - "label": "Discharge" + "fieldtype": "Column Break" }, { "fieldname": "discharge_practitioner", @@ -192,10 +257,57 @@ "read_only": 1 }, { + "collapsible": 1, + "fieldname": "medication_section", + "fieldtype": "Section Break", + "label": "Medications", + "permlevel": 1 + }, + { + "fieldname": "drug_prescription", + "fieldtype": "Table", + "options": "Drug Prescription", + "permlevel": 1 + }, + { + "collapsible": 1, + "fieldname": "investigations_section", + "fieldtype": "Section Break", + "label": "Investigations", + "permlevel": 1 + }, + { + "fieldname": "lab_test_prescription", + "fieldtype": "Table", + "options": "Lab Prescription", + "permlevel": 1 + }, + { + "collapsible": 1, + "fieldname": "procedures_section", + "fieldtype": "Section Break", + "label": "Procedures", + "permlevel": 1 + }, + { + "fieldname": "procedure_prescription", + "fieldtype": "Table", + "options": "Procedure Prescription", + "permlevel": 1 + }, + { + "depends_on": "eval:(doc.status != \"Admission Scheduled\")", "fieldname": "sb_inpatient_occupancy", "fieldtype": "Section Break", "label": "Inpatient Occupancy" }, + { + "fieldname": "admission_service_unit_type", + "fieldtype": "Link", + "label": "Admission Service Unit Type", + "options": "Healthcare Service Unit Type", + "read_only": 1 + }, { "fieldname": "inpatient_occupancies", "fieldtype": "Table", @@ -208,14 +320,15 @@ "label": "Transfer" }, { - "depends_on": "eval:doc.status != \"Admission Scheduled\"", + "depends_on": "eval:(doc.status == \"Discharge Scheduled\" || doc.status == \"Discharged\")", "fieldname": "sb_discharge_note", "fieldtype": "Section Break", - "label": "Discharge Note" + "label": "Discharge Notes" }, { "fieldname": "discharge_note", - "fieldtype": "Text Editor" + "fieldtype": "Text Editor", + "permlevel": 1 }, { "fetch_from": "admission_encounter.company", @@ -224,10 +337,81 @@ "in_standard_filter": 1, "label": "Company", "options": "Company" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval:(doc.status == \"Admitted\")", + "fieldname": "encounter_details_section", + "fieldtype": "Section Break", + "label": "Encounter Impression", + "permlevel": 1 + }, + { + "fieldname": "column_break_29", + "fieldtype": "Column Break" + }, + { + "fieldname": "diagnosis", + "fieldtype": "Table MultiSelect", + "label": "Diagnosis", + "options": "Patient Encounter Diagnosis", + "permlevel": 1 + }, + { + "fieldname": "followup_date", + "fieldtype": "Date", + "label": "Follow Up Date" + }, + { + "collapsible": 1, + "depends_on": "eval:(doc.status == \"Discharge Scheduled\" || doc.status == \"Discharged\")", + "fieldname": "sb_discharge_details", + "fieldtype": "Section Break", + "label": "Discharge Detials" + }, + { + "fieldname": "discharge_instructions", + "fieldtype": "Small Text", + "label": "Discharge Instructions" + }, + { + "fieldname": "discharge_ordered_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Discharge Ordered Date", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "rehabilitation_section", + "fieldtype": "Section Break", + "label": "Rehabilitation", + "permlevel": 1 + }, + { + "fieldname": "therapy_plan", + "fieldtype": "Link", + "hidden": 1, + "label": "Therapy Plan", + "options": "Therapy Plan", + "permlevel": 1, + "read_only": 1 + }, + { + "fieldname": "therapies", + "fieldtype": "Table", + "options": "Therapy Plan Detail", + "permlevel": 1 + }, + { + "fieldname": "discharge_date", + "fieldtype": "Date", + "label": "Discharge Date", + "read_only": 1 } ], "links": [], - "modified": "2020-04-07 13:13:39.351977", + "modified": "2020-05-21 02:26:22.144575", "modified_by": "Administrator", "module": "Healthcare", "name": "Inpatient Record", @@ -244,6 +428,42 @@ "role": "Healthcare Administrator", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Nursing User", + "share": 1, + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "role": "Physician", + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "report": 1, + "role": "Nursing User" } ], "restrict_to_domain": "Healthcare", diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index 835b38bedf..cf63b65f4d 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +import frappe, json from frappe import _ from frappe.utils import today, now_datetime, getdate from frappe.model.document import Document @@ -11,8 +11,12 @@ from frappe.desk.reportview import get_match_cond class InpatientRecord(Document): def after_insert(self): - frappe.db.set_value("Patient", self.patient, "inpatient_status", "Admission Scheduled") - frappe.db.set_value("Patient", self.patient, "inpatient_record", self.name) + 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) def validate(self): self.validate_dates() @@ -22,13 +26,10 @@ class InpatientRecord(Document): frappe.db.set_value("Patient", self.patient, "inpatient_record", None) def validate_dates(self): - if (getdate(self.scheduled_date) < getdate(today())) or \ - (getdate(self.admitted_datetime) < getdate(today())): - frappe.throw(_("Scheduled and Admitted dates can not be less than today")) if (getdate(self.expected_discharge) < getdate(self.scheduled_date)) or \ - (getdate(self.discharge_date) < getdate(self.scheduled_date)): - frappe.throw(_("Expected and Discharge dates cannot be less than Admission Schedule date")) - + (getdate(self.discharge_ordered_date) < getdate(self.scheduled_date)): + frappe.throw(_('Expected and Discharge dates cannot be less than Admission Schedule date')) + def validate_already_scheduled_or_admitted(self): query = """ select name, status @@ -59,37 +60,76 @@ class InpatientRecord(Document): if service_unit: transfer_patient(self, service_unit, check_in) + @frappe.whitelist() -def schedule_inpatient(patient, encounter_id, practitioner): - patient_obj = frappe.get_doc('Patient', patient) +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')) + 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 - inpatient_record.blood_group = patient_obj.blood_group - inpatient_record.dob = patient_obj.dob - inpatient_record.mobile = patient_obj.mobile - inpatient_record.email = patient_obj.email - inpatient_record.phone = patient_obj.phone - inpatient_record.status = "Admission Scheduled" + + # Admission order details + set_details_from_ip_order(inpatient_record, admission_order) + + # Patient details + 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 + inpatient_record.blood_group = patient.blood_group + inpatient_record.dob = patient.dob + inpatient_record.mobile = patient.mobile + inpatient_record.email = patient.email + inpatient_record.phone = patient.phone inpatient_record.scheduled_date = today() - inpatient_record.admission_practitioner = practitioner - inpatient_record.admission_encounter = encounter_id + + # 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) + + 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.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.therapies: # Therapies + inpatient_record.therapy_plan = encounter.therapy_plan + set_ip_child_records(inpatient_record, 'therapies', encounter.therapies) + + inpatient_record.status = 'Admission Scheduled' inpatient_record.save(ignore_permissions = True) @frappe.whitelist() -def schedule_discharge(patient, encounter_id=None, practitioner=None): - inpatient_record_id = frappe.db.get_value('Patient', patient, 'inpatient_record') +def schedule_discharge(args): + discharge_order = json.loads(args) + 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) - inpatient_record.discharge_practitioner = practitioner - inpatient_record.discharge_encounter = encounter_id - inpatient_record.status = "Discharge Scheduled" - + inpatient_record = frappe.get_doc('Inpatient Record', inpatient_record_id) check_out_inpatient(inpatient_record) - + 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", patient, "inpatient_status", "Discharge Scheduled") + 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): + for key in ip_order: + inpatient_record.set(key, ip_order[key]) + +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'): + table.set(df.fieldname, item.get(df.fieldname)) def check_out_inpatient(inpatient_record): if inpatient_record.inpatient_occupancies: @@ -128,7 +168,7 @@ def validate_invoiced_inpatient(inpatient_record): if pending_invoices: frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", " - .join(pending_invoices))) + .join(pending_invoices)), title=_('Unbilled Invoices')) def get_pending_doc(doc, doc_name_list, pending_invoices): if doc_name_list: @@ -144,19 +184,19 @@ def get_pending_doc(doc, doc_name_list, pending_invoices): return pending_invoices def get_inpatient_docs_not_invoiced(doc, inpatient_record): - return frappe.db.get_list(doc, filters = {"patient": inpatient_record.patient, - "inpatient_record": inpatient_record.name, "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', []) 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', {}) diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py index 4c2d3f692a..2bef5fb5bd 100644 --- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py @@ -15,6 +15,7 @@ class TestInpatientRecord(unittest.TestCase): patient = create_patient() # Schedule Admission ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 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")) @@ -26,7 +27,7 @@ class TestInpatientRecord(unittest.TestCase): self.assertEqual("Occupied", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) # Discharge - schedule_discharge(patient=patient) + schedule_discharge(frappe.as_json({'patient': patient})) self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) @@ -44,8 +45,10 @@ class TestInpatientRecord(unittest.TestCase): patient = create_patient() ip_record = create_inpatient(patient) + ip_record.expected_length_of_stay = 0 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) service_unit = get_healthcare_service_unit() diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json index 17dc1edd8c..88eeb46a24 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.json +++ b/erpnext/healthcare/doctype/lab_test/lab_test.json @@ -33,9 +33,10 @@ "user", "invoiced", "sb_first", + "template", "lab_test_name", "column_break_26", - "template", + "medical_code", "lab_test_group", "sb_normal", "normal_test_items", @@ -424,11 +425,19 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "fetch_from": "template.medical_code", + "fieldname": "medical_code", + "fieldtype": "Link", + "label": "Medical Code", + "options": "Medical Code", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-04-04 19:16:29.131168", + "modified": "2020-06-29 14:24:26.509721", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test", diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.js b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.js index 5c9bf49e60..c3eedbbdf1 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.js +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Lab Test Template",{ if (!frm.doc.lab_test_description) frm.set_value("lab_test_description", frm.doc.lab_test_name); }, - refresh : function(frm) { + refresh: function(frm) { // Restrict Special, Grouped type templates in Child TestGroups frm.set_query("lab_test_template", "lab_test_groups", function() { return { @@ -17,6 +17,15 @@ frappe.ui.form.on("Lab Test Template",{ } }; }); + }, + medical_code: function(frm) { + frm.set_query("medical_code", function() { + return { + filters: { + medical_code_standard: frm.doc.medical_code_standard + } + }; + }); } }); diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json index a606bc4b1d..ebd2ec0246 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json @@ -19,6 +19,9 @@ "disabled", "is_billable", "lab_test_rate", + "medical_coding_section", + "medical_code_standard", + "medical_code", "section_break_normal", "lab_test_uom", "lab_test_normal_range", @@ -237,10 +240,29 @@ "fieldtype": "Text", "ignore_xss_filter": 1, "label": "Collection Details" + }, + { + "collapsible": 1, + "fieldname": "medical_coding_section", + "fieldtype": "Section Break", + "label": "Medical Coding" + }, + { + "depends_on": "medical_code_standard", + "fieldname": "medical_code", + "fieldtype": "Link", + "label": "Medical Code", + "options": "Medical Code" + }, + { + "fieldname": "medical_code_standard", + "fieldtype": "Link", + "label": "Medical Code Standard", + "options": "Medical Code Standard" } ], "links": [], - "modified": "2020-03-25 16:53:01.740103", + "modified": "2020-06-29 14:07:20.772219", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test Template", 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 e2b47b4559..3521561f34 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py @@ -115,7 +115,7 @@ def make_item_price(item, price_list_name, item_price): "price_list": price_list_name, "item_code": item, "price_list_rate": item_price - }).insert(ignore_permissions=True) + }).insert(ignore_permissions=True, ignore_mandatory=True) @frappe.whitelist() def change_test_code_from_template(lab_test_code, doc): diff --git a/erpnext/healthcare/doctype/medical_code/medical_code.json b/erpnext/healthcare/doctype/medical_code/medical_code.json index a2e7247517..5d69830907 100644 --- a/erpnext/healthcare/doctype/medical_code/medical_code.json +++ b/erpnext/healthcare/doctype/medical_code/medical_code.json @@ -1,156 +1,69 @@ { - "allow_copy": 1, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "beta": 1, - "creation": "2017-06-21 13:02:56.122897", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_copy": 1, + "allow_import": 1, + "allow_rename": 1, + "beta": 1, + "creation": "2017-06-21 13:02:56.122897", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "medical_code_standard", + "code", + "description" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "medical_code_standard", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Medical Code Standard", - "length": 0, - "no_copy": 0, - "options": "Medical Code Standard", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "medical_code_standard", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Medical Code Standard", + "options": "Medical Code Standard", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "code", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 1, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Code", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "code", + "fieldtype": "Data", + "ignore_xss_filter": 1, + "in_list_view": 1, + "label": "Code", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 1, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "bold": 1, + "fieldname": "description", + "fieldtype": "Small Text", + "ignore_xss_filter": 1, + "in_list_view": 1, + "label": "Description" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-10-04 17:08:11.053418", - "modified_by": "Administrator", - "module": "Healthcare", - "name": "Medical Code", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-06-29 14:02:30.980032", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Medical Code", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Physician", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Healthcare", - "search_fields": "code, description", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "", - "track_changes": 1, - "track_seen": 0 + ], + "quick_entry": 1, + "restrict_to_domain": "Healthcare", + "search_fields": "code, description", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index b8a400c6b7..ac35acc21a 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -39,9 +39,9 @@ "section_break_16", "mode_of_payment", "billing_item", + "invoiced", "column_break_2", "paid_amount", - "invoiced", "ref_sales_invoice", "section_break_3", "referring_practitioner", @@ -348,7 +348,7 @@ } ], "links": [], - "modified": "2020-04-27 21:36:06.404062", + "modified": "2020-05-21 03:04:21.400893", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 9eb6e77c85..512fb48360 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -447,7 +447,7 @@ def get_prescribed_therapies(patient): """ SELECT t.therapy_type, t.name, t.parent, e.practitioner, - e.encounter_date, e.therapy_plan, e.visit_department + e.encounter_date, e.therapy_plan, e.medical_department FROM `tabPatient Encounter` e, `tabTherapy Plan Detail` t WHERE diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json index 3952a8153f..15c94344e9 100644 --- a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json +++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json @@ -11,6 +11,7 @@ "patient", "assessment_template", "column_break_4", + "company", "healthcare_practitioner", "assessment_datetime", "assessment_description", @@ -127,11 +128,18 @@ "fieldname": "assessment_description", "fieldtype": "Small Text", "label": "Assessment Description" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company" } ], "is_submittable": 1, "links": [], - "modified": "2020-04-21 13:23:09.815007", + "modified": "2020-05-25 14:38:38.302399", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Assessment", diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js index 2410f8e10d..edcee99d4b 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js @@ -180,35 +180,114 @@ frappe.ui.form.on('Patient Encounter', { } }); -let schedule_inpatient = function(frm) { - frappe.call({ - method: 'erpnext.healthcare.doctype.inpatient_record.inpatient_record.schedule_inpatient', - args: {patient: frm.doc.patient, encounter_id: frm.doc.name, practitioner: frm.doc.practitioner}, - callback: function(data) { - if (!data.exc) { - frm.reload_doc(); +var schedule_inpatient = function(frm) { + var dialog = new frappe.ui.Dialog({ + title: 'Patient Admission', + fields: [ + {fieldtype: 'Link', label: 'Medical Department', fieldname: 'medical_department', options: 'Medical Department', reqd: 1}, + {fieldtype: 'Link', label: 'Healthcare Practitioner (Primary)', fieldname: 'primary_practitioner', options: 'Healthcare Practitioner', reqd: 1}, + {fieldtype: 'Link', label: 'Healthcare Practitioner (Secondary)', fieldname: 'secondary_practitioner', options: 'Healthcare Practitioner'}, + {fieldtype: 'Column Break'}, + {fieldtype: 'Date', label: 'Admission Ordered For', fieldname: 'admission_ordered_for', default: 'Today'}, + {fieldtype: 'Link', label: 'Service Unit Type', fieldname: 'service_unit_type', options: 'Healthcare Service Unit Type'}, + {fieldtype: 'Int', label: 'Expected Length of Stay', fieldname: 'expected_length_of_stay'}, + {fieldtype: 'Section Break'}, + {fieldtype: 'Long Text', label: 'Admission Instructions', fieldname: 'admission_instruction'} + ], + primary_action_label: __('Order Admission'), + primary_action : function() { + var args = { + patient: frm.doc.patient, + admission_encounter: frm.doc.name, + referring_practitioner: frm.doc.practitioner, + company: frm.doc.company, + medical_department: dialog.get_value('medical_department'), + primary_practitioner: dialog.get_value('primary_practitioner'), + secondary_practitioner: dialog.get_value('secondary_practitioner'), + admission_ordered_for: dialog.get_value('admission_ordered_for'), + admission_service_unit_type: dialog.get_value('service_unit_type'), + expected_length_of_stay: dialog.get_value('expected_length_of_stay'), + admission_instruction: dialog.get_value('admission_instruction') } - }, - freeze: true, - freeze_message: __('Process Inpatient Scheduling') + frappe.call({ + method: 'erpnext.healthcare.doctype.inpatient_record.inpatient_record.schedule_inpatient', + args: { + args: args + }, + callback: function(data) { + if (!data.exc) { + frm.reload_doc(); + } + }, + freeze: true, + freeze_message: 'Scheduling Patient Admission' + }); + frm.refresh_fields(); + dialog.hide(); + } }); + + dialog.set_values({ + 'medical_department': frm.doc.medical_department, + 'primary_practitioner': frm.doc.practitioner, + }); + + dialog.fields_dict['service_unit_type'].get_query = function() { + return { + filters: { + 'inpatient_occupancy': 1, + 'allow_appointments': 0 + } + }; + }; + + dialog.show(); + dialog.$wrapper.find('.modal-dialog').css('width', '800px'); }; -let schedule_discharge = function(frm) { - frappe.call({ - method: 'erpnext.healthcare.doctype.inpatient_record.inpatient_record.schedule_discharge', - args: {patient: frm.doc.patient, encounter_id: frm.doc.name, practitioner: frm.doc.practitioner}, - callback: function(data) { - if (!data.exc) { - frm.reload_doc(); +var schedule_discharge = function(frm) { + var dialog = new frappe.ui.Dialog ({ + title: 'Inpatient Discharge', + fields: [ + {fieldtype: 'Date', label: 'Discharge Ordered Date', fieldname: 'discharge_ordered_date', default: 'Today', read_only: 1}, + {fieldtype: 'Date', label: 'Followup Date', fieldname: 'followup_date'}, + {fieldtype: 'Column Break'}, + {fieldtype: 'Small Text', label: 'Discharge Instructions', fieldname: 'discharge_instructions'}, + {fieldtype: 'Section Break', label:'Discharge Summary'}, + {fieldtype: 'Long Text', label: 'Discharge Note', fieldname: 'discharge_note'} + ], + primary_action_label: __('Order Discharge'), + primary_action : function() { + var args = { + patient: frm.doc.patient, + discharge_encounter: frm.doc.name, + discharge_practitioner: frm.doc.practitioner, + discharge_ordered_date: dialog.get_value('discharge_ordered_date'), + followup_date: dialog.get_value('followup_date'), + discharge_instructions: dialog.get_value('discharge_instructions'), + discharge_note: dialog.get_value('discharge_note') } - }, - freeze: true, - freeze_message: 'Process Discharge' + frappe.call ({ + method: 'erpnext.healthcare.doctype.inpatient_record.inpatient_record.schedule_discharge', + args: {args}, + callback: function(data) { + if(!data.exc){ + frm.reload_doc(); + } + }, + freeze: true, + freeze_message: 'Scheduling Inpatient Discharge' + }); + frm.refresh_fields(); + dialog.hide(); + } }); + + dialog.show(); + dialog.$wrapper.find('.modal-dialog').css('width', '800px'); }; -let create_medical_record = function (frm) { +let create_medical_record = function(frm) { if (!frm.doc.patient) { frappe.throw(__('Please select patient')); } @@ -221,7 +300,7 @@ let create_medical_record = function (frm) { frappe.new_doc('Patient Medical Record'); }; -let create_vital_signs = function (frm) { +let create_vital_signs = function(frm) { if (!frm.doc.patient) { frappe.throw(__('Please select patient')); } @@ -233,7 +312,7 @@ let create_vital_signs = function (frm) { frappe.new_doc('Vital Signs'); }; -let create_procedure = function (frm) { +let create_procedure = function(frm) { if (!frm.doc.patient) { frappe.throw(__('Please select patient')); } diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json index 05eec87398..15675f4673 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json @@ -52,6 +52,7 @@ ], "fields": [ { + "allow_on_submit": 1, "fieldname": "inpatient_record", "fieldtype": "Link", "label": "Inpatient Record", @@ -296,6 +297,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "fieldname": "inpatient_status", "fieldtype": "Data", "label": "Inpatient Status", @@ -326,7 +328,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-27 21:58:29.789797", + "modified": "2020-05-16 21:00:08.644531", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Encounter", diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py index 56401a3e74..262fc4650a 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py @@ -73,7 +73,7 @@ def update_encounter_medical_record(encounter): insert_encounter_to_medical_record(encounter) def delete_medical_record(encounter): - frappe.db.delete_doc_if_exists('Patient Medical Record', 'reference_name', encounter.name) + frappe.delete_doc_if_exists('Patient Medical Record', 'reference_name', encounter.name) def set_subject_field(encounter): subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '
' diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.json b/erpnext/healthcare/doctype/sample_collection/sample_collection.json index c352287faf..016cfbc3ae 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.json +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.json @@ -85,11 +85,9 @@ { "fieldname": "company", "fieldtype": "Link", - "hidden": 1, + "in_standard_filter": 1, "label": "Company", - "options": "Company", - "print_hide": 1, - "report_hide": 1 + "options": "Company" }, { "fieldname": "section_break_6", @@ -167,7 +165,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-04 19:17:02.707203", + "modified": "2020-05-25 14:36:46.990469", "modified_by": "Administrator", "module": "Healthcare", "name": "Sample Collection", diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json index ca78b6618e..9edfeb2faa 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json @@ -10,6 +10,7 @@ "patient", "patient_name", "column_break_4", + "company", "status", "start_date", "section_break_3", @@ -98,10 +99,17 @@ "label": "Status", "options": "Not Started\nIn Progress\nCompleted\nCancelled", "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company" } ], "links": [], - "modified": "2020-04-21 13:13:43.956014", + "modified": "2020-05-25 14:38:53.649315", "modified_by": "Administrator", "module": "Healthcare", "name": "Therapy Plan", diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.json b/erpnext/healthcare/doctype/therapy_session/therapy_session.json index 00d74a0949..c75d9342ef 100644 --- a/erpnext/healthcare/doctype/therapy_session/therapy_session.json +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.json @@ -19,6 +19,7 @@ "practitioner", "department", "details_section", + "medical_code", "duration", "rate", "location", @@ -206,11 +207,19 @@ "fieldtype": "Data", "label": "Patient Name", "read_only": 1 + }, + { + "fetch_from": "therapy_type.medical_code", + "fieldname": "medical_code", + "fieldtype": "Link", + "label": "Medical Code", + "options": "Medical Code", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-04-29 16:49:16.286006", + "modified": "2020-06-29 14:33:34.836594", "modified_by": "Administrator", "module": "Healthcare", "name": "Therapy Session", diff --git a/erpnext/healthcare/doctype/therapy_type/therapy_type.js b/erpnext/healthcare/doctype/therapy_type/therapy_type.js index 7a61b0def0..6e155dc21f 100644 --- a/erpnext/healthcare/doctype/therapy_type/therapy_type.js +++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.js @@ -45,6 +45,16 @@ frappe.ui.form.on('Therapy Type', { medical_department: function(frm) { mark_change_in_item(frm); + }, + + medical_code: function(frm) { + frm.set_query("medical_code", function() { + return { + filters: { + medical_code_standard: frm.doc.medical_code_standard + } + }; + }); } }); diff --git a/erpnext/healthcare/doctype/therapy_type/therapy_type.json b/erpnext/healthcare/doctype/therapy_type/therapy_type.json index 0b3c3caeaa..f365b1df03 100644 --- a/erpnext/healthcare/doctype/therapy_type/therapy_type.json +++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.json @@ -22,6 +22,9 @@ "item_group", "column_break_12", "description", + "medical_coding_section", + "medical_code_standard", + "medical_code", "section_break_18", "therapy_for", "add_exercises", @@ -160,10 +163,30 @@ { "fieldname": "section_break_18", "fieldtype": "Section Break" + }, + { + "collapsible": 1, + "fieldname": "medical_coding_section", + "fieldtype": "Section Break", + "label": "Medical Coding", + "options": "Medical Coding" + }, + { + "fieldname": "medical_code_standard", + "fieldtype": "Link", + "label": "Medical Code Standard", + "options": "Medical Code Standard" + }, + { + "depends_on": "medical_code_standard", + "fieldname": "medical_code", + "fieldtype": "Link", + "label": "Medical Code", + "options": "Medical Code" } ], "links": [], - "modified": "2020-04-21 13:09:04.006289", + "modified": "2020-06-29 14:18:50.669951", "modified_by": "Administrator", "module": "Healthcare", "name": "Therapy Type", diff --git a/erpnext/healthcare/module_onboarding/healthcare/healthcare.json b/erpnext/healthcare/module_onboarding/healthcare/healthcare.json new file mode 100644 index 0000000000..56c3c13559 --- /dev/null +++ b/erpnext/healthcare/module_onboarding/healthcare/healthcare.json @@ -0,0 +1,41 @@ +{ + "allow_roles": [ + { + "role": "Healthcare Administrator" + } + ], + "creation": "2020-05-19 10:32:43.025852", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/healthcare", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-08 14:06:19.512946", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Healthcare", + "owner": "Administrator", + "steps": [ + { + "step": "Create Patient" + }, + { + "step": "Create Practitioner Schedule" + }, + { + "step": "Introduction to Healthcare Practitioner" + }, + { + "step": "Create Healthcare Practitioner" + }, + { + "step": "Explore Healthcare Settings" + }, + { + "step": "Explore Clinical Procedure Templates" + } + ], + "subtitle": "Patients, Practitioner Schedules, Settings, and more.", + "success_message": "The Healthcare Module is all set up!", + "title": "Let's Set Up the Healthcare Module." +} \ No newline at end of file diff --git a/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json b/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json new file mode 100644 index 0000000000..c45a347080 --- /dev/null +++ b/erpnext/healthcare/onboarding_step/create_healthcare_practitioner/create_healthcare_practitioner.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-19 10:39:55.728058", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-26 23:16:31.965521", + "modified_by": "Administrator", + "name": "Create Healthcare Practitioner", + "owner": "Administrator", + "reference_document": "Healthcare Practitioner", + "show_full_form": 1, + "title": "Create Healthcare Practitioner", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/onboarding_step/create_patient/create_patient.json b/erpnext/healthcare/onboarding_step/create_patient/create_patient.json new file mode 100644 index 0000000000..77bc5bd7ad --- /dev/null +++ b/erpnext/healthcare/onboarding_step/create_patient/create_patient.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-19 10:32:27.648902", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:26:24.023418", + "modified_by": "Administrator", + "name": "Create Patient", + "owner": "Administrator", + "reference_document": "Patient", + "show_full_form": 1, + "title": "Create Patient", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json b/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json new file mode 100644 index 0000000000..65980ef668 --- /dev/null +++ b/erpnext/healthcare/onboarding_step/create_practitioner_schedule/create_practitioner_schedule.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-19 10:41:19.065753", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:27:09.437825", + "modified_by": "Administrator", + "name": "Create Practitioner Schedule", + "owner": "Administrator", + "reference_document": "Practitioner Schedule", + "show_full_form": 1, + "title": "Create Practitioner Schedule", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json b/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json new file mode 100644 index 0000000000..697b761e52 --- /dev/null +++ b/erpnext/healthcare/onboarding_step/explore_clinical_procedure_templates/explore_clinical_procedure_templates.json @@ -0,0 +1,19 @@ +{ + "action": "Show Form Tour", + "creation": "2020-05-19 11:40:51.963741", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-26 23:10:24.504030", + "modified_by": "Administrator", + "name": "Explore Clinical Procedure Templates", + "owner": "Administrator", + "reference_document": "Clinical Procedure Template", + "show_full_form": 0, + "title": "Explore Clinical Procedure Templates", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json b/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json new file mode 100644 index 0000000000..b2d5aef431 --- /dev/null +++ b/erpnext/healthcare/onboarding_step/explore_healthcare_settings/explore_healthcare_settings.json @@ -0,0 +1,19 @@ +{ + "action": "Show Form Tour", + "creation": "2020-05-19 11:14:33.044989", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 1, + "is_skipped": 0, + "modified": "2020-05-26 23:10:24.507648", + "modified_by": "Administrator", + "name": "Explore Healthcare Settings", + "owner": "Administrator", + "reference_document": "Healthcare Settings", + "show_full_form": 0, + "title": "Explore Healthcare Settings", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json b/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json new file mode 100644 index 0000000000..fa4c9036d7 --- /dev/null +++ b/erpnext/healthcare/onboarding_step/introduction_to_healthcare_practitioner/introduction_to_healthcare_practitioner.json @@ -0,0 +1,20 @@ +{ + "action": "Show Form Tour", + "creation": "2020-05-19 10:43:56.231679", + "docstatus": 0, + "doctype": "Onboarding Step", + "field": "schedule", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-26 22:07:07.482530", + "modified_by": "Administrator", + "name": "Introduction to Healthcare Practitioner", + "owner": "Administrator", + "reference_document": "Healthcare Practitioner", + "show_full_form": 0, + "title": "Introduction to Healthcare Practitioner", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/healthcare/setup.py b/erpnext/healthcare/setup.py index 2087f49f32..06840801d3 100644 --- a/erpnext/healthcare/setup.py +++ b/erpnext/healthcare/setup.py @@ -195,10 +195,21 @@ def create_sensitivity(): def add_healthcare_service_unit_tree_root(): record = [ - { - "doctype": "Healthcare Service Unit", - "healthcare_service_unit_name": "All Healthcare Service Units", - "is_group": 1 - } + { + "doctype": "Healthcare Service Unit", + "healthcare_service_unit_name": "All Healthcare Service Units", + "is_group": 1, + "company": get_company() + } ] insert_record(record) + +def get_company(): + company = frappe.defaults.get_defaults().company + if company: + return company + else: + company = frappe.get_list("Company", limit=1) + if company: + return company[0].name + return None diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index f092578003..9abaa0784a 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -512,10 +512,10 @@ def get_children(doctype, parent, company, is_root=False): def get_patient_vitals(patient, from_date=None, to_date=None): if not patient: return - vitals = frappe.db.get_all('Vital Signs', { + vitals = frappe.db.get_all('Vital Signs', filters={ 'docstatus': 1, 'patient': patient - }, order_by='signs_date, signs_time') + }, order_by='signs_date, signs_time', fields=['*']) if len(vitals): return vitals diff --git a/erpnext/hooks.py b/erpnext/hooks.py index ab161aa9f5..e8dda207ec 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -13,7 +13,7 @@ source_link = "https://github.com/frappe/erpnext" app_logo_url = '/assets/erpnext/images/erp-icon.svg' -develop_version = '12.x.x-develop' +develop_version = '13.x.x-develop' app_include_js = "assets/js/erpnext.min.js" app_include_css = "assets/css/erpnext.css" @@ -41,7 +41,7 @@ boot_session = "erpnext.startup.boot.boot_session" notification_config = "erpnext.startup.notifications.get_notification_config" get_help_messages = "erpnext.utilities.activation.get_help_messages" leaderboards = "erpnext.startup.leaderboard.get_leaderboards" - +filters_config = "erpnext.startup.filters.get_filters_config" on_session_creation = [ "erpnext.portal.utils.create_customer_or_supplier", @@ -234,10 +234,20 @@ doc_events = { "validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products" }, "Sales Invoice": { - "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"], - "on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel", + "on_submit": [ + "erpnext.regional.create_transaction_log", + "erpnext.regional.italy.utils.sales_invoice_on_submit", + "erpnext.erpnext_integrations.taxjar_integration.create_transaction" + ], + "on_cancel": [ + "erpnext.regional.italy.utils.sales_invoice_on_cancel", + "erpnext.erpnext_integrations.taxjar_integration.delete_transaction" + ], "on_trash": "erpnext.regional.check_deletion_permission" }, + "Purchase Invoice": { + "validate": "erpnext.regional.india.utils.update_grand_total_for_rcm" + }, "Payment Entry": { "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status"], "on_trash": "erpnext.regional.check_deletion_permission" @@ -258,6 +268,9 @@ doc_events = { }, "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"] } } @@ -320,8 +333,7 @@ scheduler_events = { "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans" ], "monthly_long": [ - "erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income", - "erpnext.accounts.deferred_revenue.convert_deferred_expense_to_expense", + "erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.hr.utils.allocate_earned_leaves", "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" ] @@ -362,7 +374,8 @@ regional_overrides = { '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.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.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', + 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.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' diff --git a/erpnext/hr/dashboard_fixtures.py b/erpnext/hr/dashboard_fixtures.py new file mode 100644 index 0000000000..1e9b4f3c93 --- /dev/null +++ b/erpnext/hr/dashboard_fixtures.py @@ -0,0 +1,190 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import erpnext +import json +from frappe import _ + +def get_data(): + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(), + "number_cards": get_number_cards(), + }) + +def get_dashboards(): + dashboards = [] + dashboards.append(get_human_resource_dashboard()) + return dashboards + +def get_human_resource_dashboard(): + return { + "name": "Human Resource", + "dashboard_name": "Human Resource", + "is_default": 1, + "charts": [ + { "chart": "Attendance Count", "width": "Full"}, + { "chart": "Gender Diversity Ratio", "width": "Half"}, + { "chart": "Job Application Status", "width": "Half"}, + { "chart": 'Designation Wise Employee Count', "width": "Half"}, + { "chart": 'Department Wise Employee Count', "width": "Half"}, + { "chart": 'Designation Wise Openings', "width": "Half"}, + { "chart": 'Department Wise Openings', "width": "Half"} + ], + "cards": [ + {"card": "Total Employees"}, + {"card": "New Joinees (Last year)"}, + {'card': "Employees Left (Last year)"}, + {'card': "Total Applicants (Last month)"}, + ] + } + +def get_recruitment_dashboard(): + pass + + +def get_charts(): + company = erpnext.get_default_company() + date = frappe.utils.get_datetime() + + month_map = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov","Dec"] + + + if not company: + company = frappe.db.get_value("Company", {"is_group": 0}, "name") + + dashboard_charts = [ + get_dashboards_chart_doc('Gender Diversity Ratio', "Group By", "Pie", + document_type = "Employee", group_by_type="Count", group_by_based_on="gender", + filters_json = json.dumps([["Employee", "status", "=", "Active"]])) + ] + + dashboard_charts.append( + get_dashboards_chart_doc('Job Application Status', "Group By", "Pie", + document_type = "Job Applicant", group_by_type="Count", group_by_based_on="status", + filters_json = json.dumps([["Job Applicant", "creation", "Previous", "1 month"]])) + ) + + custom_options = '''{ + "type": "line", + "axisOptions": { + "shortenYAxisNumbers": 1 + }, + "tooltipOptions": {} + }''' + + filters_json = json.dumps({ + "month": month_map[date.month - 1], + "year": str(date.year), + "company":company + }) + + dashboard_charts.append( + get_dashboards_chart_doc('Attendance Count', "Report", "Line", + report_name = "Monthly Attendance Sheet", is_custom =1, group_by_type="Count", + filters_json = filters_json, custom_options=custom_options) + ) + + dashboard_charts.append( + get_dashboards_chart_doc('Department Wise Employee Count', "Group By", "Donut", + document_type = "Employee", group_by_type="Count", group_by_based_on="department", + filters_json = json.dumps([["Employee", "status", "=", "Active"]])) + ) + + dashboard_charts.append( + get_dashboards_chart_doc('Designation Wise Employee Count', "Group By", "Donut", + document_type = "Employee", group_by_type="Count", group_by_based_on="designation", + filters_json = json.dumps([["Employee", "status", "=", "Active"]])) + ) + + dashboard_charts.append( + get_dashboards_chart_doc('Designation Wise Openings', "Group By", "Bar", + document_type = "Job Opening", group_by_type="Sum", group_by_based_on="designation", + time_interval = "Monthly", aggregate_function_based_on = "planned_vacancies") + ) + dashboard_charts.append( + get_dashboards_chart_doc('Department Wise Openings', "Group By", "Bar", + document_type = "Job Opening", group_by_type="Sum", group_by_based_on="department", + time_interval = "Monthly", aggregate_function_based_on = "planned_vacancies") + ) + return dashboard_charts + + +def get_number_cards(): + number_cards = [] + + number_cards = [ + get_number_cards_doc("Employee", "Total Employees", filters_json = json.dumps([ + ["Employee","status","=","Active"] + ]) + ) + ] + + number_cards.append( + get_number_cards_doc("Employee", "New Joinees (Last year)", filters_json = json.dumps([ + ["Employee","date_of_joining","Timespan","last year"], + ["Employee","status","=","Active"] + ]) + ) + ) + + number_cards.append( + get_number_cards_doc("Employee", "Employees Left (Last year)", filters_json = json.dumps([ + ["Employee", "relieving_date", "Timespan", "last year"], + ["Employee", "status", "=", "Left"] + ]) + ) + ) + + number_cards.append( + get_number_cards_doc("Job Applicant", "Total Applicants (Last month)", filters_json = json.dumps([ + ["Job Applicant", "creation", "Timespan", "last month"] + ]) + ) + ) + + return number_cards + + +def get_number_cards_doc(document_type, label, **args): + args = frappe._dict(args) + + return { + "doctype": "Number Card", + "document_type": document_type, + "function": args.func or "Count", + "is_public": args.is_public or 1, + "label": _(label), + "name": args.name or label, + "show_percentage_stats": args.show_percentage_stats or 1, + "stats_time_interval": args.stats_time_interval or 'Monthly', + "filters_json": args.filters_json or '[]', + "aggregate_function_based_on": args.aggregate_function_based_on or None + } + +def get_dashboards_chart_doc(name, chart_type, graph_type, **args): + args = frappe._dict(args) + + return { + "name": name, + "chart_name": _(args.chart_name or name), + "chart_type": chart_type, + "document_type": args.document_type or None, + "report_name": args.report_name or None, + "is_custom": args.is_custom or 0, + "group_by_type": args.group_by_type or None, + "group_by_based_on": args.group_by_based_on or None, + "based_on": args.based_on or None, + "value_based_on": args.value_based_on or None, + "number_of_groups": args.number_of_groups or 0, + "is_public": args.is_public or 1, + "timespan": args.timespan or "Last Year", + "time_interval": args.time_interval or "Yearly", + "timeseries": args.timeseries or 0, + "filters_json": args.filters_json or '[]', + "type": graph_type, + "custom_options": args.custom_options or '', + "doctype": "Dashboard Chart", + "aggregate_function_based_on": args.aggregate_function_based_on or None + } \ No newline at end of file diff --git a/erpnext/hr/desk_page/hr/hr.json b/erpnext/hr/desk_page/hr/hr.json index 22aa170744..0fed8d322f 100644 --- a/erpnext/hr/desk_page/hr/hr.json +++ b/erpnext/hr/desk_page/hr/hr.json @@ -18,12 +18,7 @@ { "hidden": 0, "label": "Leaves", - "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Application\",\n \"name\": \"Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Allocation\",\n \"name\": \"Leave Allocation\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Type\"\n ],\n \"label\": \"Leave Policy\",\n \"name\": \"Leave Policy\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Period\",\n \"name\": \"Leave Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Type\",\n \"name\": \"Leave Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Holiday List\",\n \"name\": \"Holiday List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Compensatory Leave Request\",\n \"name\": \"Compensatory Leave Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Encashment\",\n \"name\": \"Leave Encashment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Block List\",\n \"name\": \"Leave Block List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Application\"\n ],\n \"doctype\": \"Leave Application\",\n \"is_query_report\": true,\n \"label\": \"Employee Leave Balance\",\n \"name\": \"Employee Leave Balance\",\n \"type\": \"report\"\n }\n]" - }, - { - "hidden": 0, - "label": "Payroll", - "links": "[\n {\n \"label\": \"Salary Structure\",\n \"name\": \"Salary Structure\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Salary Structure\",\n \"Employee\"\n ],\n \"label\": \"Salary Structure Assignment\",\n \"name\": \"Salary Structure Assignment\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Payroll Entry\",\n \"name\": \"Payroll Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Slip\",\n \"name\": \"Salary Slip\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Payroll Period\",\n \"name\": \"Payroll Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Income Tax Slab\",\n \"name\": \"Income Tax Slab\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Component\",\n \"name\": \"Salary Component\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Additional Salary\",\n \"name\": \"Additional Salary\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Retention Bonus\",\n \"name\": \"Retention Bonus\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Incentive\",\n \"name\": \"Employee Incentive\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"is_query_report\": true,\n \"label\": \"Salary Register\",\n \"name\": \"Salary Register\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Application\",\n \"name\": \"Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Allocation\",\n \"name\": \"Leave Allocation\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Type\"\n ],\n \"label\": \"Leave Policy\",\n \"name\": \"Leave Policy\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Period\",\n \"name\": \"Leave Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Type\",\n \"name\": \"Leave Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Holiday List\",\n \"name\": \"Holiday List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Compensatory Leave Request\",\n \"name\": \"Compensatory Leave Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Encashment\",\n \"name\": \"Leave Encashment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Block List\",\n \"name\": \"Leave Block List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Application\"\n ],\n \"doctype\": \"Leave Application\",\n \"is_query_report\": true,\n \"label\": \"Employee Leave Balance\",\n \"name\": \"Employee Leave Balance\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -50,11 +45,6 @@ "label": "Recruitment", "links": "[\n {\n \"label\": \"Job Opening\",\n \"name\": \"Job Opening\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Job Applicant\",\n \"name\": \"Job Applicant\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Job Offer\",\n \"name\": \"Job Offer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Staffing Plan\",\n \"name\": \"Staffing Plan\",\n \"type\": \"doctype\"\n }\n]" }, - { - "hidden": 0, - "label": "Loans", - "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n }\n]" - }, { "hidden": 0, "label": "Training", @@ -69,34 +59,36 @@ "hidden": 0, "label": "Performance", "links": "[\n {\n \"label\": \"Appraisal\",\n \"name\": \"Appraisal\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Appraisal Template\",\n \"name\": \"Appraisal Template\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Energy Point Rule\",\n \"name\": \"Energy Point Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Energy Point Log\",\n \"name\": \"Energy Point Log\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Employee Tax and Benefits", - "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Declaration\",\n \"name\": \"Employee Tax Exemption Declaration\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Proof Submission\",\n \"name\": \"Employee Tax Exemption Proof Submission\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\",\n \"Payroll Period\"\n ],\n \"label\": \"Employee Other Income\",\n \"name\": \"Employee Other Income\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Benefit Application\",\n \"name\": \"Employee Benefit Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Benefit Claim\",\n \"name\": \"Employee Benefit Claim\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Category\",\n \"name\": \"Employee Tax Exemption Category\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Tax Exemption Sub Category\",\n \"name\": \"Employee Tax Exemption Sub Category\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Modules", - "charts": [], + "charts": [ + { + "chart_name": "Attendance Count", + "label": "Attendance Count" + } + ], "creation": "2020-03-02 15:48:58.322521", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "HR", - "modified": "2020-04-29 20:29:22.114309", + "modified": "2020-06-16 19:20:50.976045", "modified_by": "Administrator", "module": "HR", "name": "HR", + "onboarding": "Human Resource", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "shortcuts": [ { + "color": "#9deca2", "format": "{} Active", "label": "Employee", "link_to": "Employee", @@ -104,33 +96,34 @@ "type": "DocType" }, { - "format": "{} Unpaid", - "label": "Expense Claim", - "link_to": "Expense Claim", - "stats_filter": "{\"approval_status\":\"Draft\"}", + "label": "Attendance", + "link_to": "Attendance", + "stats_filter": "", "type": "DocType" }, { "format": "{} Open", - "label": "Job Applicant", - "link_to": "Job Applicant", - "stats_filter": "{\n \"status\": \"Open\"\n}", - "type": "DocType" - }, - { - "label": "Salary Structure", - "link_to": "Salary Structure", - "type": "DocType" - }, - { "label": "Leave Application", "link_to": "Leave Application", + "stats_filter": "{\"status\":\"Open\"}", "type": "DocType" }, { - "label": "Salary Register", - "link_to": "Salary Register", + "label": "Job Applicant", + "link_to": "Job Applicant", + "type": "DocType" + }, + { + "label": "Monthly Attendance Sheet", + "link_to": "Monthly Attendance Sheet", "type": "Report" + }, + { + "format": "{} Open", + "label": "Dashboard", + "link_to": "Human Resource", + "stats_filter": "{\n \"status\": \"Open\"\n}", + "type": "Dashboard" } ] } \ No newline at end of file diff --git a/erpnext/hr/doctype/appraisal_template/appraisal_template.py b/erpnext/hr/doctype/appraisal_template/appraisal_template.py index e5d3c42e1b..d0dfad4be3 100644 --- a/erpnext/hr/doctype/appraisal_template/appraisal_template.py +++ b/erpnext/hr/doctype/appraisal_template/appraisal_template.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint +from frappe.utils import cint, flt from frappe import _ from frappe.model.document import Document @@ -11,11 +11,11 @@ from frappe.model.document import Document class AppraisalTemplate(Document): def validate(self): self.check_total_points() - - def check_total_points(self): + + def check_total_points(self): total_points = 0 for d in self.get("goals"): - total_points += int(d.per_weightage or 0) + total_points += flt(d.per_weightage) if cint(total_points) != 100: frappe.throw(_("Sum of points for all goals should be 100. It is {0}").format(total_points)) diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index 906f6f77f2..a656a7ea5f 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -19,11 +19,15 @@ "attendance_date", "company", "department", - "shift", "attendance_request", - "amended_from", + "details_section", + "shift", + "in_time", + "out_time", + "column_break_18", "late_entry", - "early_exit" + "early_exit", + "amended_from" ], "fields": [ { @@ -172,13 +176,36 @@ "fieldname": "early_exit", "fieldtype": "Check", "label": "Early Exit" + }, + { + "fieldname": "details_section", + "fieldtype": "Section Break", + "label": "Details" + }, + { + "depends_on": "shift", + "fieldname": "in_time", + "fieldtype": "Datetime", + "label": "In Time", + "read_only": 1 + }, + { + "depends_on": "shift", + "fieldname": "out_time", + "fieldtype": "Datetime", + "label": "Out Time", + "read_only": 1 + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" } ], "icon": "fa fa-ok", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-11 11:40:14.319496", + "modified": "2020-05-29 13:51:37.177231", "modified_by": "Administrator", "module": "HR", "name": "Attendance", diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 6c7ee002db..45b7060610 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -172,8 +172,8 @@ def get_unmarked_days(employee, month): records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [ - ["attendance_date", ">", month_start], - ["attendance_date", "<", month_end], + ["attendance_date", ">=", month_start], + ["attendance_date", "<=", month_end], ["employee", "=", employee], ["docstatus", "!=", 2] ]) diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index df0f75a18c..d4c118f802 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -19,7 +19,7 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): approvers = [] department_details = {} department_list = [] - employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver"], as_dict=True) + employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver"], as_dict=True) employee_department = filters.get("department") or employee.department if employee_department: @@ -33,10 +33,16 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): 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'])) + 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'])) + + if filters.get("doctype") == "Leave Application": parentfield = "leave_approvers" + field_name = "Leave Approver" else: parentfield = "expense_approvers" + field_name = "Expense Approver" if department_list: for d in department_list: approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from @@ -46,4 +52,12 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): and approver.parentfield = %s and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) + if len(approvers) == 0: + frappe.throw(_("Please set {0} for the Employee or for Department: {1}"). + format( + field_name, frappe.bold(employee_department), + frappe.bold(employee.name) + ), + title=_(field_name + " Missing")) + return set(tuple(approver) for approver in approvers) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index f575765f69..f2afe065d1 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -62,6 +62,7 @@ "salary_mode", "payroll_cost_center", "column_break_52", + "expense_approver", "bank_name", "bank_ac_no", "health_insurance_section", @@ -205,7 +206,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nActive\nLeft", + "options": "Active\nLeft", "reqd": 1, "search_index": 1 }, @@ -409,6 +410,8 @@ "options": "Branch" }, { + "fetch_from": "grade.default_leave_policy", + "fetch_if_empty": 1, "fieldname": "leave_policy", "fieldtype": "Link", "label": "Leave Policy", @@ -667,6 +670,7 @@ "oldfieldtype": "Date" }, { + "depends_on": "eval:doc.status == \"Left\"", "fieldname": "relieving_date", "fieldtype": "Date", "label": "Relieving Date", @@ -797,13 +801,19 @@ { "fieldname": "column_break_52", "fieldtype": "Column Break" + }, + { + "fieldname": "expense_approver", + "fieldtype": "Link", + "label": "Expense Approver", + "options": "User" } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2020-05-05 18:51:03.152503", + "modified": "2020-07-03 21:28:04.109189", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 4d49503d2d..7338cbb6c8 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -413,7 +413,11 @@ def get_employee_emails(employee_list): @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): - filters = [['company', '=', company]] + + filters = [] + if company and company != 'All Companies': + filters = [['company', '=', company]] + fields = ['name as value', 'employee_name as title'] if is_root: diff --git a/erpnext/hr/doctype/employee/employee_tree.js b/erpnext/hr/doctype/employee/employee_tree.js index 0a2da63058..9ab091a1eb 100644 --- a/erpnext/hr/doctype/employee/employee_tree.js +++ b/erpnext/hr/doctype/employee/employee_tree.js @@ -4,7 +4,7 @@ frappe.treeview_settings['Employee'] = { { fieldname: "company", fieldtype:"Select", - options: erpnext.utils.get_tree_options("company"), + options: ['All Companies'].concat(erpnext.utils.get_tree_options("company")), label: __("Company"), default: erpnext.utils.get_tree_default("company") } diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index 6cc49cfff2..cba8ee9a40 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -139,13 +139,13 @@ frappe.ui.form.on('Employee Advance', { employee: function (frm) { if (frm.doc.employee) { return frappe.call({ - method: "erpnext.hr.doctype.employee_advance.employee_advance.get_due_advance_amount", + method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount", args: { "employee": frm.doc.employee, "posting_date": frm.doc.posting_date }, callback: function(r) { - frm.set_value("due_advance_amount",r.message); + frm.set_value("pending_amount",r.message); } }); } diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 8c5ce42d87..0d90913871 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -19,7 +19,7 @@ "column_break_11", "advance_amount", "paid_amount", - "due_advance_amount", + "pending_amount", "claimed_amount", "return_amount", "section_break_7", @@ -102,14 +102,6 @@ "options": "Company:company:default_currency", "read_only": 1 }, - { - "depends_on": "eval:cur_frm.doc.employee", - "fieldname": "due_advance_amount", - "fieldtype": "Currency", - "label": "Due Advance Amount", - "options": "Company:company:default_currency", - "read_only": 1 - }, { "fieldname": "claimed_amount", "fieldtype": "Currency", @@ -177,11 +169,19 @@ "fieldname": "repay_unclaimed_amount_from_salary", "fieldtype": "Check", "label": "Repay unclaimed amount from salary" + }, + { + "depends_on": "eval:cur_frm.doc.employee", + "fieldname": "pending_amount", + "fieldtype": "Currency", + "label": "Pending Amount", + "options": "Company:company:default_currency", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-03-06 15:11:33.747535", + "modified": "2020-06-12 12:42:39.833818", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 23e4992066..3c435b8cc3 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -22,6 +22,7 @@ class EmployeeAdvance(Document): self.validate_employee_advance_account() def on_cancel(self): + self.ignore_linked_doctypes = ('GL Entry') self.set_status() def set_status(self): @@ -95,7 +96,7 @@ class EmployeeAdvance(Document): frappe.db.set_value("Employee Advance", self.name, "status", self.status) @frappe.whitelist() -def get_due_advance_amount(employee, posting_date): +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"]) @@ -119,12 +120,14 @@ def make_bank_entry(dt, dn): "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(doc.advance_amount), "account_currency": payment_account.account_currency, "account_type": payment_account.account_type @@ -146,7 +149,7 @@ def create_return_through_additional_salary(doc): return additional_salary @frappe.whitelist() -def make_return_entry(employee_name, company, employee_advance_name, return_amount, mode_of_payment, advance_account): +def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, mode_of_payment=None): return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) mode_of_payment_type = '' diff --git a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.json deleted file mode 100644 index cf624195b7..0000000000 --- a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.json +++ /dev/null @@ -1,576 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "HR-BEN-APP-.YY.-.MM.-.#####", - "beta": 0, - "creation": "2018-04-13 16:31:39.190787", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "max_benefits", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Max Benefits (Yearly)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "remaining_benefit", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Remaining Benefits (Yearly)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "payroll_period", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Payroll Period", - "length": 0, - "no_copy": 0, - "options": "Payroll Period", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fieldname": "department", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Employee Benefit Application", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Benefits Applied", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee_benefits", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Benefits", - "length": 0, - "no_copy": 0, - "options": "Employee Benefit Application Detail", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "totals", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Totals", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "pro_rata_dispensed_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Dispensed Amount (Pro-rated)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:39.714081", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Benefit Application", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "employee_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json b/erpnext/hr/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json deleted file mode 100644 index 56421db8c3..0000000000 --- a/erpnext/hr/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2018-04-13 16:36:18.389786", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "earning_component", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Earning Component", - "length": 0, - "no_copy": 0, - "options": "Salary Component", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "earning_component.pay_against_benefit_claim", - "fieldname": "pay_against_benefit_claim", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Pay Against Benefit Claim", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "earning_component.max_benefit_amount", - "fieldname": "max_benefit_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Max Benefit Amount", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-08-21 16:15:42.111118", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Benefit Application Detail", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_benefit_claim/employee_benefit_claim.json b/erpnext/hr/doctype/employee_benefit_claim/employee_benefit_claim.json deleted file mode 100644 index 1aa69d02a5..0000000000 --- a/erpnext/hr/doctype/employee_benefit_claim/employee_benefit_claim.json +++ /dev/null @@ -1,580 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "HR-BEN-CLM-.YY.-.MM.-.#####", - "beta": 0, - "creation": "2018-04-13 16:43:10.386409", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fieldname": "department", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "claim_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Claim Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "benefit_type_and_amount", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Benefit Type and Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "earning_component", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Claim Benefit For", - "length": 0, - "no_copy": 0, - "options": "Salary Component", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "earning_component.max_benefit_amount", - "fieldname": "max_amount_eligible", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Max Amount Eligible", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "earning_component.pay_against_benefit_claim", - "fieldname": "pay_against_benefit_claim", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Pay Against Benefit Claim", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "claimed_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Claimed Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "salary_slip", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Salary Slip", - "length": 0, - "no_copy": 0, - "options": "Salary Slip", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Employee Benefit Claim", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Expense Proof", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "attachments", - "fieldtype": "Attach", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Attachments", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:35.942067", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Benefit Claim", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "employee_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.json b/erpnext/hr/doctype/employee_checkin/employee_checkin.json index 75f699751b..d34316dc0f 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.json +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.json @@ -41,8 +41,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Log Type", - "options": "\nIN\nOUT", - "reqd": 1 + "options": "\nIN\nOUT" }, { "fieldname": "shift", @@ -108,7 +107,7 @@ } ], "links": [], - "modified": "2020-01-23 04:57:42.551355", + "modified": "2020-07-08 11:02:32.660986", "modified_by": "Administrator", "module": "HR", "name": "Employee Checkin", diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index 86705121ac..15fbd4e015 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -72,7 +72,7 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N return doc -def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, 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. @@ -100,7 +100,9 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki 'company': employee_doc.company, 'shift': shift, 'late_entry': late_entry, - 'early_exit': early_exit + 'early_exit': early_exit, + 'in_time': in_time, + 'out_time': out_time } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() diff --git a/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py b/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py index 420bbe6b1a..9e7d3186b8 100644 --- a/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import unittest from frappe.utils import getdate, add_days -from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee class TestEmployeePromotion(unittest.TestCase): def setUp(self): diff --git a/erpnext/hr/doctype/employee_tax_exemption_category/employee_tax_exemption_category.json b/erpnext/hr/doctype/employee_tax_exemption_category/employee_tax_exemption_category.json deleted file mode 100644 index 66fac5bee5..0000000000 --- a/erpnext/hr/doctype/employee_tax_exemption_category/employee_tax_exemption_category.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-04-13 16:51:36.971140", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "max_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Max Exemption Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fetch_if_empty": 0, - "fieldname": "is_active", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Is Active", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-25 13:20:31.367158", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Tax Exemption Category", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json b/erpnext/hr/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json deleted file mode 100644 index 7b3b8f5caa..0000000000 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-04-13 16:56:23.333041", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "exemption_sub_category", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Exemption Sub Category", - "length": 0, - "no_copy": 0, - "options": "Employee Tax Exemption Sub Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "exemption_sub_category.exemption_category", - "fetch_if_empty": 0, - "fieldname": "exemption_category", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Exemption Category", - "length": 0, - "no_copy": 0, - "options": "Employee Tax Exemption Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "exemption_sub_category.max_amount", - "fetch_if_empty": 0, - "fieldname": "max_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Maximum Exempted Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Declared Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-04-26 11:28:14.023086", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Tax Exemption Declaration Category", - "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 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py deleted file mode 100644 index e54d9193ba..0000000000 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -import frappe -import unittest -# from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import create_exemption_category, create_payroll_period -# -# class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): -# def setup(self): -# make_employee("employee@proofsubmission.com") -# create_payroll_period() -# create_exemption_category() -# 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)] -# }) -# 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)] -# }) -# 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), -# ] -# }) -# self.assertRaises(frappe.ValidationError, declaration.save) diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json b/erpnext/hr/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json deleted file mode 100644 index b9254afad0..0000000000 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json +++ /dev/null @@ -1,213 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-04-13 17:19:03.006149", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "exemption_sub_category", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Exemption Sub Category", - "length": 0, - "no_copy": 0, - "options": "Employee Tax Exemption Sub Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "exemption_sub_category.exemption_category", - "fetch_if_empty": 0, - "fieldname": "exemption_category", - "fieldtype": "Read Only", - "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": "Exemption Category", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "exemption_sub_category.max_amount", - "fetch_if_empty": 0, - "fieldname": "max_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Maximum Exemption Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "type_of_proof", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Type of Proof", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Actual Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-04-25 15:45:03.154904", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Tax Exemption Proof Submission Detail", - "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 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.json b/erpnext/hr/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.json deleted file mode 100644 index b0e492e7ca..0000000000 --- a/erpnext/hr/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-05-09 12:47:26.983095", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "exemption_category", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Tax Exemption Category", - "length": 0, - "no_copy": 0, - "options": "Employee Tax Exemption Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "exemption_category.max_amount", - "fetch_if_empty": 1, - "fieldname": "max_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Max Exemption Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fetch_if_empty": 0, - "fieldname": "is_active", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Is Active", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-25 13:24:05.164877", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Tax Exemption Sub Category", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index fb2310396b..221300b519 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -113,6 +113,14 @@ cur_frm.cscript.calculate_total_amount = function(doc,cdt,cdn){ cur_frm.cscript.calculate_total(doc,cdt,cdn); }; +cur_frm.fields_dict['cost_center'].get_query = function(doc) { + return { + filters: { + "company": doc.company + } + } +}; + erpnext.expense_claim = { set_title: function(frm) { if (!frm.doc.task) { @@ -213,12 +221,15 @@ frappe.ui.form.on("Expense Claim", { refresh: function(frm) { frm.trigger("toggle_fields"); - if(frm.doc.docstatus === 1 && frm.doc.approval_status !== "Rejected") { + if(frm.doc.docstatus > 0 && frm.doc.approval_status !== "Rejected") { frm.add_custom_button(__('Accounting Ledger'), function() { frappe.route_options = { voucher_no: frm.doc.name, company: frm.doc.company, - group_by_voucher: false + from_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), + group_by: '', + show_cancelled_entries: frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); }, __("View")); @@ -243,7 +254,6 @@ frappe.ui.form.on("Expense Claim", { }, update_employee_advance_claimed_amount: function(frm) { - console.log("update_employee_advance_claimed_amount") let amount_to_be_allocated = frm.doc.grand_total; $.each(frm.doc.advances || [], function(i, advance){ if (amount_to_be_allocated >= advance.unclaimed_amount){ @@ -295,6 +305,21 @@ frappe.ui.form.on("Expense Claim", { frm.events.get_advances(frm); }, + cost_center: function(frm) { + frm.events.set_child_cost_center(frm); + }, + + validate: function(frm) { + frm.events.set_child_cost_center(frm); + }, + + set_child_cost_center: function(frm){ + (frm.doc.expenses || []).forEach(function(d) { + if (!d.cost_center){ + d.cost_center = frm.doc.cost_center; + } + }); + }, get_taxes: function(frm) { if(frm.doc.taxes) { frappe.call({ @@ -337,10 +362,6 @@ frappe.ui.form.on("Expense Claim", { }); frappe.ui.form.on("Expense Claim Detail", { - expenses_add: function(frm, cdt, cdn) { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("expenses", row, ["cost_center"]); - }, amount: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; frappe.model.set_value(cdt, cdn, 'sanctioned_amount', child.amount); diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index 96baaab595..fa28470af8 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -66,6 +66,7 @@ "fieldname": "employee", "fieldtype": "Link", "in_global_search": 1, + "in_standard_filter": 1, "label": "From Employee", "oldfieldname": "employee", "oldfieldtype": "Link", @@ -164,6 +165,7 @@ "default": "Today", "fieldname": "posting_date", "fieldtype": "Date", + "in_standard_filter": 1, "label": "Posting Date", "oldfieldname": "posting_date", "oldfieldtype": "Date", @@ -236,6 +238,7 @@ { "fieldname": "company", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Company", "oldfieldname": "company", "oldfieldtype": "Link", @@ -368,7 +371,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2019-12-14 23:52:05.388458", + "modified": "2020-06-15 12:43:04.099803", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index ea469b82c9..bf893d5fab 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -129,7 +129,7 @@ class ExpenseClaim(AccountsController): "debit": data.sanctioned_amount, "debit_in_account_currency": data.sanctioned_amount, "against": self.employee, - "cost_center": data.cost_center + "cost_center": data.cost_center or self.cost_center }, item=data) ) @@ -295,7 +295,7 @@ def make_bank_entry(dt, dn): je = frappe.new_doc("Journal 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, @@ -303,6 +303,7 @@ def make_bank_entry(dt, dn): "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 }) @@ -313,6 +314,7 @@ def make_bank_entry(dt, dn): "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 }) diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.js b/erpnext/hr/doctype/hr_settings/hr_settings.js index b629b42f9b..fd082fda09 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.js +++ b/erpnext/hr/doctype/hr_settings/hr_settings.js @@ -2,21 +2,6 @@ // For license information, please see license.txt frappe.ui.form.on('HR Settings', { - encrypt_salary_slips_in_emails: function(frm) { - let encrypt_state = frm.doc.encrypt_salary_slips_in_emails; - frm.set_df_property('password_policy', 'reqd', encrypt_state); - }, - - validate: function(frm) { - let policy = frm.doc.password_policy; - if (policy) { - if (policy.includes(' ') || policy.includes('--')) { - frappe.msgprint(__("Password policy cannot contain spaces or simultaneous hyphens. The format will be restructured automatically")); - } - frm.set_value('password_policy', policy.split(new RegExp(" |-", 'g')).filter((token) => token).join('-')); - } - }, - restrict_backdated_leave_application: function(frm) { frm.toggle_reqd("role_allowed_to_create_backdated_leave_application", frm.doc.restrict_backdated_leave_application); } diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index ebf8723be6..c42e1d72fc 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -12,16 +12,6 @@ "column_break_4", "stop_birthday_reminders", "expense_approver_mandatory_in_expense_claim", - "payroll_settings", - "payroll_based_on", - "max_working_hours_against_timesheet", - "include_holidays_in_total_working_days", - "disable_rounded_total", - "column_break_11", - "daily_wages_fraction_for_half_day", - "email_salary_slip_to_employee", - "encrypt_salary_slips_in_emails", - "password_policy", "leave_settings", "leave_approval_notification_template", "leave_status_notification_template", @@ -38,13 +28,17 @@ { "fieldname": "employee_settings", "fieldtype": "Section Break", - "label": "Employee Settings" + "label": "Employee Settings", + "show_days": 1, + "show_seconds": 1 }, { "description": "Enter retirement age in years", "fieldname": "retirement_age", "fieldtype": "Data", - "label": "Retirement Age" + "label": "Retirement Age", + "show_days": 1, + "show_seconds": 1 }, { "default": "Naming Series", @@ -52,161 +46,126 @@ "fieldname": "emp_created_by", "fieldtype": "Select", "label": "Employee Records to be created by", - "options": "Naming Series\nEmployee Number\nFull Name" + "options": "Naming Series\nEmployee Number\nFull Name", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_4", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "description": "Don't send Employee Birthday Reminders", "fieldname": "stop_birthday_reminders", "fieldtype": "Check", - "label": "Stop Birthday Reminders" + "label": "Stop Birthday Reminders", + "show_days": 1, + "show_seconds": 1 }, { "default": "1", "fieldname": "expense_approver_mandatory_in_expense_claim", "fieldtype": "Check", - "label": "Expense Approver Mandatory In Expense Claim" - }, - { - "fieldname": "payroll_settings", - "fieldtype": "Section Break", - "label": "Payroll Settings" - }, - { - "default": "0", - "description": "If checked, Total no. of Working Days will include holidays, and this will reduce the value of Salary Per Day", - "fieldname": "include_holidays_in_total_working_days", - "fieldtype": "Check", - "label": "Include holidays in Total no. of Working Days" - }, - { - "fieldname": "max_working_hours_against_timesheet", - "fieldtype": "Float", - "label": "Max working hours against Timesheet" - }, - { - "fieldname": "column_break_11", - "fieldtype": "Column Break" - }, - { - "default": "1", - "description": "Emails salary slip to employee based on preferred email selected in Employee", - "fieldname": "email_salary_slip_to_employee", - "fieldtype": "Check", - "label": "Email Salary Slip to Employee" - }, - { - "default": "0", - "depends_on": "eval: doc.email_salary_slip_to_employee == 1;", - "description": "The salary slip emailed to the employee will be password protected, the password will be generated based on the password policy.", - "fieldname": "encrypt_salary_slips_in_emails", - "fieldtype": "Check", - "label": "Encrypt Salary Slips in Emails" - }, - { - "depends_on": "eval: doc.encrypt_salary_slips_in_emails == 1", - "description": "Example: SAL-{first_name}-{date_of_birth.year}
This will generate a password like SAL-Jane-1972", - "fieldname": "password_policy", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Password Policy" + "label": "Expense Approver Mandatory In Expense Claim", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "leave_settings", "fieldtype": "Section Break", - "label": "Leave Settings" + "label": "Leave Settings", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "leave_approval_notification_template", "fieldtype": "Link", "label": "Leave Approval Notification Template", - "options": "Email Template" + "options": "Email Template", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "leave_status_notification_template", "fieldtype": "Link", "label": "Leave Status Notification Template", - "options": "Email Template" + "options": "Email Template", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_18", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "1", "fieldname": "leave_approver_mandatory_in_leave_application", "fieldtype": "Check", - "label": "Leave Approver Mandatory In Leave Application" + "label": "Leave Approver Mandatory In Leave Application", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "show_leaves_of_all_department_members_in_calendar", "fieldtype": "Check", - "label": "Show Leaves Of All Department Members In Calendar" + "label": "Show Leaves Of All Department Members In Calendar", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "hiring_settings", "fieldtype": "Section Break", - "label": "Hiring Settings" + "label": "Hiring Settings", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "check_vacancies", "fieldtype": "Check", - "label": "Check Vacancies On Job Offer Creation" + "label": "Check Vacancies On Job Offer Creation", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "auto_leave_encashment", "fieldtype": "Check", - "label": "Auto Leave Encashment" - }, - { - "default": "0", - "description": "If checked, hides and disables Rounded Total field in Salary Slips", - "fieldname": "disable_rounded_total", - "fieldtype": "Check", - "label": "Disable Rounded Total" + "label": "Auto Leave Encashment", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "restrict_backdated_leave_application", "fieldtype": "Check", - "label": "Restrict Backdated Leave Application" + "label": "Restrict Backdated Leave Application", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.restrict_backdated_leave_application == 1", "fieldname": "role_allowed_to_create_backdated_leave_application", "fieldtype": "Link", "label": "Role Allowed to Create Backdated Leave Application", - "options": "Role" - }, - { - "default": "Leave", - "fieldname": "payroll_based_on", - "fieldtype": "Select", - "label": "Calculate Payroll Working Days Based On", - "options": "Leave\nAttendance" - }, - { - "default": "0.5", - "description": "The fraction of daily wages to be paid for half-day attendance", - "fieldname": "daily_wages_fraction_for_half_day", - "fieldtype": "Float", - "label": "Daily Wages Fraction for Half Day" + "options": "Role", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2020-05-11 13:02:51.274347", + "modified": "2020-06-04 15:15:09.865476", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index 5ed4c87c62..ced98fb9a5 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -5,34 +5,14 @@ from __future__ import unicode_literals import frappe -from frappe import _ from frappe.model.document import Document -from frappe.utils import cint -from frappe.custom.doctype.property_setter.property_setter import make_property_setter class HRSettings(Document): def validate(self): self.set_naming_series() - self.validate_password_policy() - - if not self.daily_wages_fraction_for_half_day: - self.daily_wages_fraction_for_half_day = 0.5 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) - def validate_password_policy(self): - if self.email_salary_slip_to_employee and self.encrypt_salary_slips_in_emails: - 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") - make_property_setter("Salary Slip", "rounded_total", "print_hide", self.disable_rounded_total, "Check") diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_list.js b/erpnext/hr/doctype/job_applicant/job_applicant_list.js new file mode 100644 index 0000000000..3b9141ba79 --- /dev/null +++ b/erpnext/hr/doctype/job_applicant/job_applicant_list.js @@ -0,0 +1,15 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.listview_settings['Job Applicant'] = { + add_fields: ["company", "designation", "job_applicant", "status"], + get_indicator: function (doc) { + if (doc.status == "Accepted") { + return [__(doc.status), "green", "status,=," + doc.status]; + } else if (["Open", "Replied"].includes(doc.status)) { + return [__(doc.status), "orange", "status,=," + doc.status]; + } else if (["Hold", "Rejected"].includes(doc.status)) { + return [__(doc.status), "red", "status,=," + doc.status]; + } + } +}; diff --git a/erpnext/hr/doctype/job_offer/job_offer.json b/erpnext/hr/doctype/job_offer/job_offer.json index ccbfdc5383..c0b7f69e1b 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.json +++ b/erpnext/hr/doctype/job_offer/job_offer.json @@ -30,7 +30,6 @@ { "fieldname": "job_applicant", "fieldtype": "Link", - "in_list_view": 1, "label": "Job Applicant", "options": "Job Applicant", "print_hide": 1, @@ -161,7 +160,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2019-12-31 02:40:33.650728", + "modified": "2020-06-25 00:56:24.756395", "modified_by": "Administrator", "module": "HR", "name": "Job Offer", diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index 32f1b89f49..f9ee44a4de 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.py +++ b/erpnext/hr/doctype/job_offer/job_offer.py @@ -20,10 +20,9 @@ class JobOffer(Document): staffing_plan = get_staffing_plan_detail(self.designation, self.company, self.offer_date) check_vacancies = frappe.get_single("HR Settings").check_vacancies if staffing_plan and check_vacancies: - vacancies = frappe.db.get_value("Staffing Plan Detail", filters={"name": staffing_plan.name}, fieldname=['vacancies']) - job_offers = len(self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date)) - if vacancies - job_offers <= 0: - frappe.throw(_("There are no vacancies under staffing plan {0}").format(get_link_to_form("Staffing Plan", staffing_plan.parent))) + job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date) + if staffing_plan.vacancies - len(job_offers) <= 0: + frappe.throw(_("There are no vacancies under staffing plan {0}").format(frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)))) def on_change(self): update_job_applicant(self.status, self.job_applicant) @@ -33,7 +32,8 @@ class JobOffer(Document): return frappe.get_all("Job Offer", filters={ "offer_date": ['between', (from_date, to_date)], "designation": self.designation, - "company": self.company + "company": self.company, + "docstatus": 1 }, fields=['name']) def update_job_applicant(status, job_applicant): @@ -42,18 +42,22 @@ def update_job_applicant(status, job_applicant): def get_staffing_plan_detail(designation, company, offer_date): detail = frappe.db.sql(""" - SELECT spd.name as name, + SELECT DISTINCT spd.parent, sp.from_date as from_date, sp.to_date as to_date, - sp.name as parent + sp.name, + sum(spd.vacancies) as vacancies, + spd.designation FROM `tabStaffing Plan Detail` spd, `tabStaffing Plan` sp WHERE sp.docstatus=1 AND spd.designation=%s 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) - return detail[0] if detail else None + + return frappe._dict(detail[0]) if detail else None @frappe.whitelist() def make_employee(source_name, target_doc=None): diff --git a/erpnext/hr/doctype/job_offer/job_offer_list.js b/erpnext/hr/doctype/job_offer/job_offer_list.js new file mode 100644 index 0000000000..4fa5be7cc8 --- /dev/null +++ b/erpnext/hr/doctype/job_offer/job_offer_list.js @@ -0,0 +1,15 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.listview_settings['Job Offer'] = { + add_fields: ["company", "designation", "job_applicant", "status"], + get_indicator: function (doc) { + if (doc.status == "Accepted") { + return [__(doc.status), "green", "status,=," + doc.status]; + } else if (doc.status == "Awaiting Response") { + return [__(doc.status), "orange", "status,=," + doc.status]; + } else if (doc.status == "Rejected") { + return [__(doc.status), "red", "status,=," + doc.status]; + } + } +}; diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 1f50e27098..4001a45507 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -38,11 +38,17 @@ frappe.ui.form.on("Leave Application", { }, validate: function(frm) { + if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1){ + frm.doc.half_day_date = frm.doc.from_date; + }else if (frm.doc.half_day == 0){ + frm.doc.half_day_date = ""; + } frm.toggle_reqd("half_day_date", frm.doc.half_day == 1); }, make_dashboard: function(frm) { var leave_details; + let lwps; if (frm.doc.employee) { frappe.call({ method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details", @@ -58,6 +64,7 @@ frappe.ui.form.on("Leave Application", { if (!r.exc && r.message['leave_approver']) { frm.set_value('leave_approver', r.message['leave_approver']); } + lwps = r.message["lwps"]; } }); $("div").remove(".form-dashboard-section.custom"); @@ -67,6 +74,18 @@ frappe.ui.form.on("Leave Application", { }) ); frm.dashboard.show(); + let allowed_leave_types = Object.keys(leave_details); + + // lwps should be allowed, lwps don't have any allocation + allowed_leave_types = allowed_leave_types.concat(lwps); + + frm.set_query('leave_type', function(){ + return { + filters : [ + ['leave_type_name', 'in', allowed_leave_types] + ] + }; + }); } }, diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json index 74707a24b8..7f50ace766 100644 --- a/erpnext/hr/doctype/leave_application/leave_application.json +++ b/erpnext/hr/doctype/leave_application/leave_application.json @@ -174,7 +174,8 @@ "label": "Status", "no_copy": 1, "options": "Open\nApproved\nRejected\nCancelled", - "permlevel": 1 + "permlevel": 1, + "reqd": 1 }, { "fieldname": "sb10", @@ -189,14 +190,14 @@ "reqd": 1 }, { + "fetch_from": "employee.company", "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company", "read_only": 1, "remember_last_selected_value": 1, - "reqd": 1, - "fetch_from": "employee.company" + "reqd": 1 }, { "allow_on_submit": 1, @@ -249,7 +250,7 @@ "is_submittable": 1, "links": [], "max_attachments": 3, - "modified": "2020-03-10 22:40:43.487721", + "modified": "2020-05-18 13:00:41.577327", "modified_by": "Administrator", "module": "HR", "name": "Leave Application", @@ -334,4 +335,4 @@ "sort_order": "DESC", "timeline_field": "employee", "title_field": "employee_name" -} +} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index d2620bec91..3f25f58383 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -19,7 +19,6 @@ class NotAnOptionalHoliday(frappe.ValidationError): pass from frappe.model.document import Document class LeaveApplication(Document): - def get_feed(self): return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type) @@ -33,6 +32,7 @@ class LeaveApplication(Document): self.validate_block_days() 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'): self.validate_optional_leave() self.validate_applicable_after() @@ -290,6 +290,12 @@ class LeaveApplication(Document): frappe.throw(_("{0} is not in Optional Holiday List").format(formatdate(day)), NotAnOptionalHoliday) day = add_days(day, 1) + def set_half_day_date(self): + if self.from_date == self.to_date and self.half_day == 1: + self.half_day_date = self.from_date + elif self.half_day == 0: + self.half_day_date = None + def notify_employee(self): employee = frappe.get_doc("Employee", self.employee) if not employee.user_id: @@ -436,21 +442,36 @@ def get_leave_details(employee, date): leave_allocation = {} 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 + 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) leave_allocation[d] = { - "total_leaves": allocation.total_leaves_allocated, + "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} + #is used in set query + lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1}) + lwps = [lwp.name for lwp in lwps] + ret = { 'leave_allocation': leave_allocation, - 'leave_approver': get_leave_approver(employee) + 'leave_approver': get_leave_approver(employee), + 'lwps': lwps } return ret @@ -596,7 +617,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date): is_carry_forward, is_expired FROM `tabLeave Ledger Entry` WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 + AND docstatus=1 AND (leaves<0 OR is_expired=1) AND (from_date between %(from_date)s AND %(to_date)s diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html index 2385b6ac1c..d30e3b9f9c 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html @@ -1,22 +1,23 @@ -{% if data %} +{% if not jQuery.isEmptyObject(data) %}
{{ __("Allocated Leaves") }}
- - - - - + + + + + + - {% for(const [key, value] of Object.entries(data)) { %} + @@ -24,6 +25,6 @@ {% } %}
{{ __("Leave Type") }}{{ __("Total Allocated Leaves") }}{{ __("Used Leaves") }}{{ __("Pending Leaves") }}{{ __("Available Leaves") }}{{ __("Leave Type") }}{{ __("Total Allocated Leaves") }}{{ __("Expired Leaves") }}{{ __("Used Leaves") }}{{ __("Pending Leaves") }}{{ __("Available Leaves") }}
{%= key %} {%= value["total_leaves"] %} {%= value["expired_leaves"] %} {%= value["leaves_taken"] %} {%= value["pending_leaves"] %} {%= value["remaining_leaves"] %}
-{% } else { %} +{% else %}

No Leaves have been allocated.

-{% } %} \ No newline at end of file +{% endif %} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 50a08b12bc..8913c648c5 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -8,7 +8,7 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import getdate, nowdate, flt from erpnext.hr.utils import set_employee_name -from erpnext.hr.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure +from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index ac7755b23a..99f6463416 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -7,7 +7,7 @@ import frappe import unittest from frappe.utils import today, add_months from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy\ diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py index 48a204596c..ff5dc2ff3e 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py +++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from frappe import _ def get_data(): return { @@ -8,13 +9,17 @@ def get_data(): }, 'transactions': [ { - 'items': ['Employee'] - }, - { - 'items': ['Employee Grade'] + 'label': _('Employees'), + 'items': ['Employee', 'Employee Grade'] }, { + 'label': _('Leaves'), 'items': ['Leave Allocation'] }, ] - } \ No newline at end of file + } + + + + + \ No newline at end of file diff --git a/erpnext/hr/doctype/payroll_employee_detail/payroll_employee_detail.json b/erpnext/hr/doctype/payroll_employee_detail/payroll_employee_detail.json deleted file mode 100644 index 0dd3403d66..0000000000 --- a/erpnext/hr/doctype/payroll_employee_detail/payroll_employee_detail.json +++ /dev/null @@ -1,209 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-11-30 06:07:33.477781", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fieldname": "department", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.designation", - "fieldname": "designation", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Designation", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "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": "2019-01-30 11:28:16.544471", - "modified_by": "Administrator", - "module": "HR", - "name": "Payroll Employee Detail", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 1, - "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 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/payroll_period_date/payroll_period_date.json b/erpnext/hr/doctype/payroll_period_date/payroll_period_date.json deleted file mode 100644 index 29bd2a3322..0000000000 --- a/erpnext/hr/doctype/payroll_period_date/payroll_period_date.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-04-13 15:17:30.513630", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Start Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "End Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-04-13 19:39:37.473294", - "modified_by": "Administrator", - "module": "HR", - "name": "Payroll Period Date", - "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 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_slip_timesheet/salary_slip_timesheet.json b/erpnext/hr/doctype/salary_slip_timesheet/salary_slip_timesheet.json deleted file mode 100644 index 797f8f7c02..0000000000 --- a/erpnext/hr/doctype/salary_slip_timesheet/salary_slip_timesheet.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-06-14 19:22:29.811658", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "time_sheet", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Time Sheet", - "length": 0, - "no_copy": 0, - "options": "Timesheet", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "working_hours", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Working Hours", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "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": "2019-02-19 08:33:41.762144", - "modified_by": "Administrator", - "module": "HR", - "name": "Salary Slip Timesheet", - "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": 0, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index d56080eecd..19735648aa 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -28,13 +28,14 @@ class ShiftType(Document): 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 = 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, 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) def get_attendance(self, logs): - """Return attendance_status, working_hours for a set of logs belonging to a single shift. + """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 @@ -48,10 +49,10 @@ class ShiftType(Document): 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 + 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 - return 'Present', total_working_hours, late_entry, early_exit + 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. diff --git a/erpnext/hr/doctype/taxable_salary_slab/taxable_salary_slab.json b/erpnext/hr/doctype/taxable_salary_slab/taxable_salary_slab.json deleted file mode 100644 index a094f8a197..0000000000 --- a/erpnext/hr/doctype/taxable_salary_slab/taxable_salary_slab.json +++ /dev/null @@ -1,232 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-04-13 17:42:13.516032", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "From Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "To Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "percent_deduction", - "fieldtype": "Percent", - "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": "Percent Deduction", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "condition", - "fieldtype": "Code", - "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": "Condition", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "html_6", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "options": "

Condition Examples

\n
    \n
  1. Applying tax if employee born between 31-12-1937 and 01-01-1958 (Employees aged 60 to 80)
    \nCondition: date_of_birth>date(1937, 12, 31) and date_of_birth<date(1958, 01, 01)

  2. Applying tax by employee gender
    \nCondition: gender==\"Male\"

  3. \n
  4. Applying tax by Salary Component
    \nCondition: base > 10000
", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-06-19 10:10:23.732132", - "modified_by": "Administrator", - "module": "HR", - "name": "Taxable Salary Slab", - "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 -} diff --git a/erpnext/hr/doctype/training_event/test_training_event.py b/erpnext/hr/doctype/training_event/test_training_event.py index 57123e304f..313f90eba8 100644 --- a/erpnext/hr/doctype/training_event/test_training_event.py +++ b/erpnext/hr/doctype/training_event/test_training_event.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import unittest from frappe.utils import today, add_days -from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee class TestTrainingEvent(unittest.TestCase): def setUp(self): diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py index 61faea1871..edf05e827b 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py @@ -22,6 +22,9 @@ def get_template(): args = frappe.local.form_dict + if getdate(args.from_date) > getdate(args.to_date): + frappe.throw(_("To Date should be greater than From Date")) + w = UnicodeWriter() w = add_header(w) diff --git a/erpnext/hr/module_onboarding/human_resource/human_resource.json b/erpnext/hr/module_onboarding/human_resource/human_resource.json new file mode 100644 index 0000000000..518c002bca --- /dev/null +++ b/erpnext/hr/module_onboarding/human_resource/human_resource.json @@ -0,0 +1,50 @@ +{ + "allow_roles": [ + { + "role": "HR Manager" + }, + { + "role": "HR User" + } + ], + "creation": "2020-05-14 11:51:45.050242", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/human-resources", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-08 14:05:47.018799", + "modified_by": "Administrator", + "module": "HR", + "name": "Human Resource", + "owner": "Administrator", + "steps": [ + { + "step": "Create Department" + }, + { + "step": "Create Designation" + }, + { + "step": "Create Holiday list" + }, + { + "step": "Create Employee" + }, + { + "step": "Create Leave Type" + }, + { + "step": "Create Leave Allocation" + }, + { + "step": "Create Leave Application" + }, + { + "step": "HR Settings" + } + ], + "subtitle": "Employee, Leaves, and more.", + "success_message": "The Human Resource Module is all set up!", + "title": "Let's Set Up the Human Resource Module. " +} \ No newline at end of file diff --git a/erpnext/hr/onboarding_step/create_department/create_department.json b/erpnext/hr/onboarding_step/create_department/create_department.json new file mode 100644 index 0000000000..66a54cfc26 --- /dev/null +++ b/erpnext/hr/onboarding_step/create_department/create_department.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 11:44:34.682115", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 12:22:26.448420", + "modified_by": "Administrator", + "name": "Create Department", + "owner": "Administrator", + "reference_document": "Department", + "show_full_form": 0, + "title": "Create Department", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/hr/onboarding_step/create_designation/create_designation.json b/erpnext/hr/onboarding_step/create_designation/create_designation.json new file mode 100644 index 0000000000..c4e9cc7798 --- /dev/null +++ b/erpnext/hr/onboarding_step/create_designation/create_designation.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 11:45:07.514193", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 12:22:41.500795", + "modified_by": "Administrator", + "name": "Create Designation", + "owner": "Administrator", + "reference_document": "Designation", + "show_full_form": 0, + "title": "Create Designation", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/hr/onboarding_step/create_employee/create_employee.json b/erpnext/hr/onboarding_step/create_employee/create_employee.json new file mode 100644 index 0000000000..3aa33c6d86 --- /dev/null +++ b/erpnext/hr/onboarding_step/create_employee/create_employee.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 11:43:25.561152", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 12:26:28.629074", + "modified_by": "Administrator", + "name": "Create Employee", + "owner": "Administrator", + "reference_document": "Employee", + "show_full_form": 0, + "title": "Create Employee", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json b/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json new file mode 100644 index 0000000000..32472b4b3f --- /dev/null +++ b/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-28 11:47:34.700174", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 12:25:38.068582", + "modified_by": "Administrator", + "name": "Create Holiday list", + "owner": "Administrator", + "reference_document": "Holiday List", + "show_full_form": 1, + "title": "Create Holiday List", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json b/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json new file mode 100644 index 0000000000..fa9941e6b9 --- /dev/null +++ b/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 11:48:56.123718", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 11:48:56.123718", + "modified_by": "Administrator", + "name": "Create Leave Allocation", + "owner": "Administrator", + "reference_document": "Leave Allocation", + "show_full_form": 0, + "title": "Create Leave Allocation", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json b/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json new file mode 100644 index 0000000000..1ed074e9a1 --- /dev/null +++ b/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 11:49:45.400764", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 11:49:45.400764", + "modified_by": "Administrator", + "name": "Create Leave Application", + "owner": "Administrator", + "reference_document": "Leave Application", + "show_full_form": 0, + "title": "Create Leave Application", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json b/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json new file mode 100644 index 0000000000..8cbfc5c81f --- /dev/null +++ b/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-27 11:17:31.119312", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-20 11:17:31.119312", + "modified_by": "Administrator", + "name": "Create Leave Type", + "owner": "Administrator", + "reference_document": "Leave Type", + "show_full_form": 1, + "title": "Create Leave Type", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/hr/onboarding_step/hr_settings/hr_settings.json b/erpnext/hr/onboarding_step/hr_settings/hr_settings.json new file mode 100644 index 0000000000..0a1d0baf8a --- /dev/null +++ b/erpnext/hr/onboarding_step/hr_settings/hr_settings.json @@ -0,0 +1,19 @@ +{ + "action": "Update Settings", + "creation": "2020-05-28 13:13:52.427711", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 1, + "is_skipped": 0, + "modified": "2020-05-20 11:16:42.430974", + "modified_by": "Administrator", + "name": "HR Settings", + "owner": "Administrator", + "reference_document": "HR Settings", + "show_full_form": 0, + "title": "HR Settings", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/hr/report/department_analytics/department_analytics.json b/erpnext/hr/report/department_analytics/department_analytics.json deleted file mode 100644 index 1e26b33c53..0000000000 --- a/erpnext/hr/report/department_analytics/department_analytics.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "add_total_row": 0, - "creation": "2018-05-15 15:37:20.883263", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2018-05-15 17:19:32.934321", - "modified_by": "Administrator", - "module": "HR", - "name": "Department Analytics", - "owner": "Administrator", - "ref_doctype": "Employee", - "report_name": "Department Analytics", - "report_type": "Script Report", - "roles": [ - { - "role": "Employee" - }, - { - "role": "HR User" - }, - { - "role": "HR Manager" - } - ] -} \ No newline at end of file diff --git a/erpnext/hr/report/department_analytics/department_analytics.py b/erpnext/hr/report/department_analytics/department_analytics.py deleted file mode 100644 index b28eac43f8..0000000000 --- a/erpnext/hr/report/department_analytics/department_analytics.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ - -def execute(filters=None): - if not filters: filters = {} - - if not filters["company"]: - frappe.throw(_('{0} is mandatory').format(_('Company'))) - - columns = get_columns() - employees = get_employees(filters) - departments_result = get_department(filters) - departments = [] - if departments_result: - for department in departments_result: - departments.append(department) - chart = get_chart_data(departments,employees) - 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") + "::60", _("Company") + ":Link/Company:120" - ] - -def get_conditions(filters): - conditions = "" - if filters.get("department"): conditions += " and department = '%s'" % \ - filters["department"].replace("'", "\\'") - - 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, - branch, department, designation, - gender, company from `tabEmployee` where status = 'Active' %s""" % conditions, as_list=1) - -def get_department(filters): - return frappe.db.sql("""select name from `tabDepartment` where company = %s""", (filters["company"]), as_list=1) - -def get_chart_data(departments,employees): - if not departments: - departments = [] - datasets = [] - for department in departments: - if department: - total_employee = frappe.db.sql("""select count(*) from \ - `tabEmployee` where \ - department = %s""" ,(department[0]), as_list=1) - datasets.append(total_employee[0][0]) - chart = { - "data": { - 'labels': departments, - 'datasets': [{'name': 'Employees','values': datasets}] - } - } - chart["type"] = "bar" - return chart - diff --git a/erpnext/hr/doctype/employee_incentive/__init__.py b/erpnext/hr/report/employee_analytics/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_incentive/__init__.py rename to erpnext/hr/report/employee_analytics/__init__.py diff --git a/erpnext/hr/report/department_analytics/department_analytics.js b/erpnext/hr/report/employee_analytics/employee_analytics.js similarity index 55% rename from erpnext/hr/report/department_analytics/department_analytics.js rename to erpnext/hr/report/employee_analytics/employee_analytics.js index 29fedcd735..8620a65a90 100644 --- a/erpnext/hr/report/department_analytics/department_analytics.js +++ b/erpnext/hr/report/employee_analytics/employee_analytics.js @@ -1,7 +1,8 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +/* eslint-disable */ -frappe.query_reports["Department Analytics"] = { +frappe.query_reports["Employee Analytics"] = { "filters": [ { "fieldname":"company", @@ -11,5 +12,12 @@ frappe.query_reports["Department Analytics"] = { "default": frappe.defaults.get_user_default("Company"), "reqd": 1 }, + { + "fieldname":"parameter", + "label": __("Parameter"), + "fieldtype": "Select", + "options": ["Branch","Grade","Department","Designation", "Employment Type"], + "reqd": 1 + } ] -}; \ No newline at end of file +}; diff --git a/erpnext/hr/report/employee_analytics/employee_analytics.json b/erpnext/hr/report/employee_analytics/employee_analytics.json new file mode 100644 index 0000000000..5a7ab9a251 --- /dev/null +++ b/erpnext/hr/report/employee_analytics/employee_analytics.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 0, + "creation": "2020-05-12 13:52:50.631086", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-05-12 13:52:50.631086", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Analytics", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Employee", + "report_name": "Employee Analytics", + "report_type": "Script Report", + "roles": [ + { + "role": "Employee" + }, + { + "role": "HR User" + }, + { + "role": "HR Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/hr/report/employee_analytics/employee_analytics.py b/erpnext/hr/report/employee_analytics/employee_analytics.py new file mode 100644 index 0000000000..8f39388926 --- /dev/null +++ b/erpnext/hr/report/employee_analytics/employee_analytics.py @@ -0,0 +1,84 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + if not filters: filters = {} + + if not filters["company"]: + frappe.throw(_('{0} is mandatory').format(_('Company'))) + + columns = get_columns() + employees = get_employees(filters) + parameters_result = get_parameters(filters) + parameters = [] + if parameters_result: + for department in parameters_result: + parameters.append(department) + + 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" + ] + +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, + branch, department, designation, + gender, company from `tabEmployee` where status = 'Active' %s""" % conditions, as_list=1) + +def get_parameters(filters): + if filters.get("parameter") == "Grade": + parameter = "Employee Grade" + else: + parameter = filters.get("parameter") + + return frappe.db.sql("""select name from `tab"""+ parameter +"""` """, as_list=1) + +def get_chart_data(parameters,employees, filters): + if not parameters: + parameters = [] + datasets = [] + 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) + if total_employee[0][0]: + label.append(parameter) + datasets.append(total_employee[0][0]) + + values = [ value for value in datasets if value !=0] + + 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["type"] = "donut" + return chart + 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 97be5cd813..db1d191758 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -3,13 +3,13 @@ from __future__ import unicode_literals import frappe -from frappe.utils import flt +from frappe.utils import flt, add_days from frappe import _ -from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on, get_leave_allocation_records +from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on def execute(filters=None): if filters.to_date <= filters.from_date: - frappe.throw(_('From date can not be greater than than To date')) + frappe.throw(_('"From date" can not be greater than or equal to "To date"')) columns = get_columns() data = get_data(filters) @@ -104,14 +104,17 @@ def get_data(filters): 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, filters.from_date) - closing = get_leave_balance_on(employee.name, leave_type, filters.to_date) + opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition row.leaves_allocated = new_allocation row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0 row.opening_balance = opening row.leaves_taken = leaves_taken - row.closing_balance = closing + + # not be shown on the basis of days left it create in user mind for carry_forward leave + row.closing_balance = (new_allocation + opening - (row.leaves_expired + leaves_taken)) + + row.indent = 1 data.append(row) new_leaves_allocated = 0 @@ -177,7 +180,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): }, as_dict=1) for record in records: - if record.to_date <= getdate(to_date): + if record.to_date < getdate(to_date): expired_leaves += record.leaves if record.from_date >= getdate(from_date): 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 a5cdecf36a..92715d3445 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 @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.utils import flt from erpnext.hr.doctype.leave_application.leave_application \ - import get_leave_balance_on, get_leaves_for_period + import get_leave_details from erpnext.hr.report.employee_leave_balance.employee_leave_balance \ import get_department_leave_approver_map @@ -61,14 +61,14 @@ def get_data(filters, leave_types): 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 - opening = get_leave_balance_on(employee.name, leave_type, filters.date) + remaining = available_leave["leave_allocation"][leave_type]['remaining_leaves'] - - row += [opening] + row += [remaining] data.append(row) 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 82ed27715f..47daab1901 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py @@ -30,8 +30,11 @@ day_abbr = [ def execute(filters=None): if not filters: filters = {} + if filters.hide_year_field == 1: + filters.year = 2020 + conditions, filters = get_conditions(filters) - columns = get_columns(filters) + columns, days = get_columns(filters) att_map = get_attendance_list(conditions, filters) if filters.group_by: @@ -60,20 +63,67 @@ def execute(filters=None): columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"]) if filters.group_by: + emp_att_map = {} for parameter in group_by_parameters: data.append([ ""+ parameter + ""]) - record = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, leave_list=leave_list) + record, aaa = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list) + emp_att_map.update(aaa) data += record else: - record = add_data(emp_map, att_map, filters, holiday_map, conditions, leave_list=leave_list) + record, emp_att_map = add_data(emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list) data += record - return columns, data + 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 = [ + {"name": "Absent", "values": []}, + {"name": "Present", "values": []}, + {"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": + total_absent_on_day += 1 + if emp_att_map[emp][idx] in ["P", "WFH"]: + total_present_on_day += 1 + if emp_att_map[emp][idx] == "HD": + total_present_on_day += 0.5 + total_leave_on_day += 0.5 + if emp_att_map[emp][idx] == "L": + total_leave_on_day += 1 -def add_data(employee_map, att_map, filters, holiday_map, conditions, leave_list=None): + 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["type"] = "line" + + return chart + +def add_data(employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=None): record = [] + emp_att_map = {} for emp in employee_map: emp_det = employee_map.get(emp) if not emp_det or emp not in att_map: @@ -85,6 +135,7 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, leave_list row += [emp, emp_det.employee_name] 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 status = att_map.get(emp).get(day + 1) @@ -101,19 +152,11 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, leave_list status = "Holiday" total_h += 1 + abbr = status_map.get(status, "") + emp_status_map.append(abbr) - # if emp_holiday_list in holiday_map and (day+1) in holiday_map[emp_holiday_list][0]: - # if holiday_map[emp_holiday_list][1]: - # status= "Weekly Off" - # else: - # status = "Holiday" - - # += 1 - - if not filters.summarized_view: - row.append(status_map.get(status, "")) - else: - if status == "Present": + if filters.summarized_view: + if status == "Present" or status == "Work From Home": total_p += 1 elif status == "Absent": total_a += 1 @@ -126,6 +169,9 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, leave_list elif not status: total_um += 1 + if not filters.summarized_view: + row += emp_status_map + if filters.summarized_view: row += [total_p, total_l, total_a, total_h, total_um] @@ -159,10 +205,10 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, leave_list row.append("0.0") row.extend([time_default_counts[0][0],time_default_counts[0][1]]) + emp_att_map[emp] = emp_status_map record.append(row) - - return record + return record, emp_att_map def get_columns(filters): @@ -172,17 +218,19 @@ def get_columns(filters): columns = [_(filters.group_by)+ ":Link/Branch:120"] columns += [ - _("Employee") + ":Link/Employee:120", _("Employee Name") + ":Link/Employee:120" + _("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) + day_name = day_abbr[getdate(date).weekday()] + days.append(cstr(day+1)+ " " +day_name +"::65") if not filters.summarized_view: - for day in range(filters["total_days_in_month"]): - date = str(filters.year) + "-" + str(filters.month)+ "-" + str(day+1) - day_name = day_abbr[getdate(date).weekday()] - columns.append(cstr(day+1)+ " " +day_name +"::65") - else: + 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"] - return columns + return columns, days def get_attendance_list(conditions, filters): attendance_list = frappe.db.sql("""select employee, day(attendance_date) as day_of_month, diff --git a/erpnext/hr/report/salary_register/salary_register.json b/erpnext/hr/report/salary_register/salary_register.json deleted file mode 100644 index 89a7ba2910..0000000000 --- a/erpnext/hr/report/salary_register/salary_register.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2017-01-10 17:36:58.153863", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2017-02-24 19:58:33.143974", - "modified_by": "Administrator", - "module": "HR", - "name": "Salary Register", - "owner": "Administrator", - "ref_doctype": "Salary Slip", - "report_name": "Salary Register", - "report_type": "Script Report", - "roles": [ - { - "role": "HR User" - }, - { - "role": "HR Manager" - } - ] -} \ No newline at end of file diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index cd125108c6..8d95924681 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -73,11 +73,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) diff --git a/erpnext/loan_management/desk_page/loan_management/loan_management.json b/erpnext/loan_management/desk_page/loan/loan.json similarity index 75% rename from erpnext/loan_management/desk_page/loan_management/loan_management.json rename to erpnext/loan_management/desk_page/loan/loan.json index 6cabff9443..3bdd1ce56e 100644 --- a/erpnext/loan_management/desk_page/loan_management/loan_management.json +++ b/erpnext/loan_management/desk_page/loan/loan.json @@ -23,7 +23,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Loan Repayment\"\n ],\n \"doctype\": \"Loan Repayment\",\n \"incomplete_dependencies\": [\n \"Loan Repayment\"\n ],\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Loan Security Pledge\"\n ],\n \"doctype\": \"Loan Security Pledge\",\n \"incomplete_dependencies\": [\n \"Loan Security Pledge\"\n ],\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -36,18 +36,21 @@ "extends_another_page": 0, "idx": 0, "is_standard": 1, - "label": "Loan Management", - "modified": "2020-05-16 08:01:27.784621", + "label": "Loan", + "modified": "2020-06-07 19:42:14.947902", "modified_by": "Administrator", "module": "Loan Management", - "name": "Loan Management", + "name": "Loan", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "shortcuts": [ { + "color": "#ffe8cd", + "format": "{} Open", "label": "Loan Application", "link_to": "Loan Application", + "stats_filter": "{ \"status\": \"Open\" }", "type": "DocType" }, { diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 9cd8b2e90a..ffef60b6b0 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -45,15 +45,6 @@ frappe.ui.form.on('Loan', { }); }) - frm.set_query('loan_security_pledge', function(doc, cdt, cdn) { - return { - filters: { - applicant: frm.doc.applicant, - docstatus: 1, - loan_application: frm.doc.loan_application || '' - } - }; - }); }, refresh: function (frm) { @@ -86,9 +77,6 @@ frappe.ui.form.on('Loan', { frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan); }, - is_secured_loan: function(frm) { - frm.toggle_reqd("loan_security_pledge", frm.doc.is_secured_loan); - }, make_loan_disbursement: function (frm) { frappe.call({ diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index b04e82274e..192beee7e3 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -25,15 +25,12 @@ "disbursement_date", "disbursed_amount", "column_break_11", + "maximum_loan_amount", "is_term_loan", "repayment_method", "repayment_periods", "monthly_repayment_amount", "repayment_start_date", - "loan_security_details_section", - "loan_security_pledge", - "column_break_25", - "maximum_loan_value", "account_info", "mode_of_payment", "payment_account", @@ -292,13 +289,8 @@ "default": "0", "fieldname": "is_secured_loan", "fieldtype": "Check", - "label": "Is Secured Loan" - }, - { - "depends_on": "is_secured_loan", - "fieldname": "loan_security_details_section", - "fieldtype": "Section Break", - "label": "Loan Security Details" + "label": "Is Secured Loan", + "read_only": 1 }, { "default": "0", @@ -324,12 +316,6 @@ "options": "Company:company:default_currency", "read_only": 1 }, - { - "fieldname": "loan_security_pledge", - "fieldtype": "Link", - "label": "Loan Security Pledge", - "options": "Loan Security Pledge" - }, { "fieldname": "disbursed_amount", "fieldtype": "Currency", @@ -338,21 +324,17 @@ "read_only": 1 }, { - "fetch_from": "loan_security_pledge.maximum_loan_value", - "fieldname": "maximum_loan_value", + "fetch_from": "loan_application.maximum_loan_amount", + "fieldname": "maximum_loan_amount", "fieldtype": "Currency", - "label": "Maximum Loan Value", + "label": "Maximum Loan Amount", "options": "Company:company:default_currency", "read_only": 1 - }, - { - "fieldname": "column_break_25", - "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2020-04-13 13:16:10.192624", + "modified": "2020-07-02 20:46:40.128142", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 76e10e5ddd..e20b484fc0 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -13,11 +13,9 @@ from erpnext.controllers.accounts_controller import AccountsController class Loan(AccountsController): def validate(self): self.set_loan_amount() - + self.validate_loan_amount() self.set_missing_fields() self.validate_accounts() - self.validate_loan_security_pledge() - self.validate_loan_amount() self.check_sanctioned_amount_limit() self.validate_repay_from_salary() @@ -56,21 +54,6 @@ class Loan(AccountsController): if self.repayment_method == "Repay Over Number of Periods": self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) - def validate_loan_security_pledge(self): - - if self.is_secured_loan and not self.loan_security_pledge: - frappe.throw(_("Loan Security Pledge is mandatory for secured loan")) - - if self.loan_security_pledge: - loan_security_details = frappe.db.get_value("Loan Security Pledge", self.loan_security_pledge, - ['loan', 'company'], as_dict=1) - - if loan_security_details.loan: - frappe.throw(_("Loan Security Pledge already pledged against loan {0}").format(loan_security_details.loan)) - - if loan_security_details.company != self.company: - frappe.throw(_("Loan Security Pledge Company and Loan Company must be same")) - def check_sanctioned_amount_limit(self): total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) @@ -129,22 +112,29 @@ class Loan(AccountsController): 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') - if not self.loan_amount and self.is_secured_loan and self.loan_security_pledge: - self.loan_amount = self.maximum_loan_value def validate_loan_amount(self): - if self.is_secured_loan and self.loan_amount > self.maximum_loan_value: - msg = _("Loan amount cannot be greater than {0}").format(self.maximum_loan_value) + if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount: + msg = _("Loan amount cannot be greater than {0}").format(self.maximum_loan_amount) frappe.throw(msg) if not self.loan_amount: frappe.throw(_("Loan amount is mandatory")) def link_loan_security_pledge(self): - frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET - loan = %s, status = 'Pledged', pledge_time = %s - where name = %s """, (self.name, now_datetime(), self.loan_security_pledge)) + if self.is_secured_loan: + loan_security_pledge = frappe.db.get_value('Loan Security Pledge', {'loan_application': self.loan_application}, + 'name') + + if loan_security_pledge: + frappe.db.set_value('Loan Security Pledge', loan_security_pledge, { + 'loan': self.name, + 'status': 'Pledged', + 'pledge_time': now_datetime() + }) def unlink_loan_security_pledge(self): frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET @@ -235,8 +225,10 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as @frappe.whitelist() def create_loan_security_unpledge(loan, applicant_type, applicant, company, as_dict=1): loan_security_pledge_details = frappe.db.sql(""" - SELECT p.parent, p.loan_security, p.qty as qty FROM `tabLoan Security Pledge` lsp , `tabPledge` p + SELECT p.loan_security, sum(p.qty) as qty + FROM `tabLoan Security Pledge` lsp , `tabPledge` p WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 + GROUP BY p.loan_security """,(loan), as_dict=1) unpledge_request = frappe.new_doc("Loan Security Unpledge") diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 364e2ffecf..c65996e65f 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -9,13 +9,14 @@ import unittest from frappe.utils import (nowdate, add_days, getdate, now_datetime, add_to_date, get_datetime, add_months, get_first_day, get_last_day, flt, date_diff) from erpnext.selling.doctype.customer.test_customer import get_customer_dict -from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (process_loan_interest_accrual_for_demand_loans, process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty +from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge class TestLoan(unittest.TestCase): def setUp(self): @@ -72,31 +73,31 @@ class TestLoan(unittest.TestCase): self.assertEquals(loan.total_payment, 302712) def test_loan_with_security(self): - pledges = [] - pledges.append({ + + pledge = [{ "loan_security": "Test Security 1", "qty": 4000.00, - "haircut": 50, - "loan_security_price": 500.00 - }) + }] - loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) - - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name) + 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) self.assertEquals(loan.loan_amount, 1000000) def test_loan_disbursement(self): - pledges = [] - pledges.append({ + pledge = [{ "loan_security": "Test Security 1", - "qty": 4000.00, - "haircut": 50 - }) + "qty": 4000.00 + }] - loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + loan_application = create_loan_application('_Test Company', self.applicant2, 'Stock Loan', pledge, "Repay Over Number of Periods", 12) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name) + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) self.assertEquals(loan.loan_amount, 1000000) loan.submit() @@ -121,18 +122,15 @@ class TestLoan(unittest.TestCase): self.assertTrue(gl_entries2) def test_regular_loan_repayment(self): - pledges = [] - pledges.append({ + pledge = [{ "loan_security": "Test Security 1", - "qty": 4000.00, - "haircut": 50 - }) + "qty": 4000.00 + }] - loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) - - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, - posting_date=get_first_day(nowdate())) + 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=get_first_day(nowdate())) loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -166,16 +164,15 @@ class TestLoan(unittest.TestCase): penalty_amount - amounts[0], 2)) def test_loan_closure_repayment(self): - pledges = [] - pledges.append({ + pledge = [{ "loan_security": "Test Security 1", - "qty": 4000.00, - "haircut": 50 - }) + "qty": 4000.00 + }] - loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, - posting_date=get_first_day(nowdate())) + 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=get_first_day(nowdate())) loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -214,23 +211,21 @@ class TestLoan(unittest.TestCase): self.assertEquals(loan.status, "Loan Closure Requested") def test_loan_repayment_for_term_loan(self): - pledges = [] - pledges.append({ + pledges = [{ "loan_security": "Test Security 2", - "qty": 4000.00, - "haircut": 50 - }) - - pledges.append({ + "qty": 4000.00 + }, + { "loan_security": "Test Security 1", - "qty": 2000.00, - "haircut": 50 - }) + "qty": 2000.00 + }] - loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + 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_security_pledge.name, 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() @@ -250,16 +245,18 @@ class TestLoan(unittest.TestCase): self.assertEquals(amounts[1], 78303.00) def test_security_shortfall(self): - pledges = [] - pledges.append({ + pledges = [{ "loan_security": "Test Security 2", "qty": 8000.00, "haircut": 50, - }) + }] - loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_security_pledge.name) + create_pledge(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) @@ -279,16 +276,15 @@ class TestLoan(unittest.TestCase): where loan_security='Test Security 2'""") def test_loan_security_unpledge(self): - pledges = [] - pledges.append({ + pledge = [{ "loan_security": "Test Security 1", - "qty": 4000.00, - "haircut": 50 - }) + "qty": 4000.00 + }] - loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, - posting_date=get_first_day(nowdate())) + 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=get_first_day(nowdate())) loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -446,12 +442,13 @@ def create_loan_security(): "haircut": 50.00, }).insert(ignore_permissions=True) -def create_loan_security_pledge(applicant, pledges): +def create_loan_security_pledge(applicant, pledges, loan_application): lsp = frappe.new_doc("Loan Security Pledge") lsp.applicant_type = 'Customer' lsp.applicant = applicant lsp.company = "_Test Company" + lsp.loan_application = loan_application for pledge in pledges: lsp.append('securities', { @@ -510,6 +507,31 @@ def create_repayment_entry(loan, applicant, posting_date, payment_type, paid_amo return lr +def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None, + repayment_periods=None, posting_date=None): + 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 + loan_application.posting_date = posting_date or nowdate() + loan_application.is_secured_loan = 1 + + if repayment_method: + loan_application.repayment_method = repayment_method + loan_application.repayment_periods = repayment_periods + + for pledge in proposed_pledges: + loan_application.append('proposed_pledges', pledge) + + loan_application.save() + loan_application.submit() + + loan_application.status = 'Approved' + loan_application.save() + + return loan_application.name + def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods, repayment_start_date=None, posting_date=None): @@ -531,14 +553,13 @@ def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_p loan.save() return loan -def create_loan_with_security(applicant, loan_type, repayment_method, repayment_periods, loan_security_pledge, - posting_date=None, repayment_start_date=None): - +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, @@ -547,7 +568,6 @@ def create_loan_with_security(applicant, loan_type, repayment_method, repayment_ "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'), - "loan_security_pledge": loan_security_pledge, "payment_account": 'Payment Account - _TC', "loan_account": 'Loan Account - _TC', "interest_income_account": 'Interest Income Account - _TC', @@ -558,19 +578,19 @@ def create_loan_with_security(applicant, loan_type, repayment_method, repayment_ return loan -def create_demand_loan(applicant, loan_type, loan_security_pledge, posting_date=None): +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'), - "loan_security_pledge": loan_security_pledge, "payment_account": 'Payment Account - _TC', "loan_account": 'Loan Account - _TC', "interest_income_account": 'Interest Income Account - _TC', diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index d3b816464f..f051755f67 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -103,10 +103,13 @@ class LoanApplication(Document): if self.is_secured_loan and not self.proposed_pledges: frappe.throw(_("Proposed Pledges are mandatory for secured Loans")) - if not self.loan_amount and self.is_secured_loan and self.proposed_pledges: - self.loan_amount = 0 + if self.is_secured_loan and self.proposed_pledges: + self.maximum_loan_amount = 0 for security in self.proposed_pledges: - self.loan_amount += security.post_haircut_amount + self.maximum_loan_amount += security.post_haircut_amount + + 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): @@ -116,7 +119,6 @@ def create_loan(source_name, target_doc=None, submit=0): filters = {'name': source_doc.loan_type} )[0] - loan_security_pledge = frappe.db.get_value("Loan Security Pledge", {"loan_application": source_name}, 'name') target_doc.mode_of_payment = account_details.mode_of_payment target_doc.payment_account = account_details.payment_account @@ -124,9 +126,6 @@ def create_loan(source_name, target_doc=None, submit=0): target_doc.interest_income_account = account_details.interest_income_account target_doc.penalty_income_account = account_details.penalty_income_account - if loan_security_pledge: - target_doc.is_secured_loan = 1 - target_doc.loan_security_pledge = loan_security_pledge doclist = get_mapped_doc("Loan Application", source_name, { "Loan Application": { 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 99c807b2cd..687c58000e 100644 --- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from erpnext.hr.doctype.salary_structure.test_salary_structure import make_employee +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee from erpnext.loan_management.doctype.loan.test_loan import create_loan_type, create_loan_accounts class TestLoanApplication(unittest.TestCase): diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index c9e36a84dd..d44088bee7 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -27,6 +27,7 @@ class LoanDisbursement(AccountsController): def on_cancel(self): self.make_gl_entries(cancel=1) + self.ignore_linked_doctypes = ['GL Entry'] def set_missing_values(self): if not self.disbursement_date: 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 0c1578ffef..2cb2637612 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -5,11 +5,12 @@ from __future__ import unicode_literals import frappe import unittest from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date) -from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry, +from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry, create_loan_application, make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_security_price) from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.selling.doctype.customer.test_customer import get_customer_dict +from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge class TestLoanDisbursement(unittest.TestCase): @@ -31,18 +32,15 @@ class TestLoanDisbursement(unittest.TestCase): self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') def test_loan_topup(self): - pledges = [] - pledges.append({ + pledge = [{ "loan_security": "Test Security 1", - "qty": 4000.00, - "haircut": 50, - "loan_security_price": 500.00 - }) + "qty": 4000.00 + }] - loan_security_pledge = create_loan_security_pledge(self.applicant, pledges) + 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_security_pledge.name, - posting_date=get_first_day(nowdate())) + loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) loan.submit() 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 9b486e805d..b56fa80c7a 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 @@ -31,6 +31,7 @@ class LoanInterestAccrual(AccountsController): self.update_is_accrued() self.make_gl_entries(cancel=1) + self.ignore_linked_doctypes = ['GL Entry'] def update_is_accrued(self): frappe.db.set_value('Repayment Schedule', self.repayment_schedule_name, 'is_accrued', 0) @@ -177,21 +178,23 @@ def get_term_loans(date, term_loan=None, loan_type=None): return term_loans def make_loan_interest_accrual_entry(args): - loan_interest_accrual = frappe.new_doc("Loan Interest Accrual") - loan_interest_accrual.loan = args.loan - loan_interest_accrual.applicant_type = args.applicant_type - loan_interest_accrual.applicant = args.applicant - loan_interest_accrual.interest_income_account = args.interest_income_account - loan_interest_accrual.loan_account = args.loan_account - loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, 2) - loan_interest_accrual.interest_amount = flt(args.interest_amount, 2) - loan_interest_accrual.posting_date = args.posting_date or nowdate() - loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest - loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name - loan_interest_accrual.payable_principal_amount = args.payable_principal + precision = cint(frappe.db.get_default("currency_precision")) or 2 - loan_interest_accrual.save() - loan_interest_accrual.submit() + loan_interest_accrual = frappe.new_doc("Loan Interest Accrual") + loan_interest_accrual.loan = args.loan + loan_interest_accrual.applicant_type = args.applicant_type + loan_interest_accrual.applicant = args.applicant + loan_interest_accrual.interest_income_account = args.interest_income_account + 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.posting_date = args.posting_date or nowdate() + loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest + loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name + loan_interest_accrual.payable_principal_amount = args.payable_principal + + loan_interest_accrual.save() + loan_interest_accrual.submit() def get_no_of_days_for_interest_accural(loan, posting_date): 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 2afed08e18..4b85b21869 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 @@ -6,10 +6,11 @@ import frappe import unittest from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date) from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_loan_security_price, - make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan) + make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_application) from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.selling.doctype.customer.test_customer import get_customer_dict +from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge class TestLoanInterestAccrual(unittest.TestCase): def setUp(self): @@ -29,17 +30,15 @@ class TestLoanInterestAccrual(unittest.TestCase): self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') def test_loan_interest_accural(self): - pledges = [] - pledges.append({ + pledge = [{ "loan_security": "Test Security 1", - "qty": 4000.00, - "haircut": 50, - "loan_security_price": 500.00 - }) + "qty": 4000.00 + }] - loan_security_pledge = create_loan_security_pledge(self.applicant, pledges) + 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_security_pledge.name, + loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) loan.submit() diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 2ab668a0e1..9605045777 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext import json from frappe import _ -from frappe.utils import flt, getdate +from frappe.utils import flt, getdate, cint from six import iteritems from frappe.model.document import Document from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day, get_datetime @@ -29,8 +29,11 @@ class LoanRepayment(AccountsController): def on_cancel(self): self.mark_as_unpaid() self.make_gl_entries(cancel=1) + self.ignore_linked_doctypes = ['GL Entry'] def set_missing_values(self, amounts): + precision = cint(frappe.db.get_default("currency_precision")) or 2 + if not self.posting_date: self.posting_date = get_datetime() @@ -38,24 +41,26 @@ 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'], 2) + self.interest_payable = flt(amounts['interest_amount'], precision) if not self.penalty_amount: - self.penalty_amount = flt(amounts['penalty_amount'], 2) + self.penalty_amount = flt(amounts['penalty_amount'], precision) if not self.pending_principal_amount: - self.pending_principal_amount = flt(amounts['pending_principal_amount'], 2) + 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'], 2) + self.payable_principal_amount = flt(amounts['payable_principal_amount'], precision) if not self.payable_amount: - self.payable_amount = flt(amounts['payable_amount'], 2) + self.payable_amount = flt(amounts['payable_amount'], precision) if amounts.get('due_date'): self.due_date = amounts.get('due_date') def validate_amount(self): + precision = cint(frappe.db.get_default("currency_precision")) or 2 + if not self.amount_paid: frappe.throw(_("Amount paid cannot be zero")) @@ -63,11 +68,13 @@ class LoanRepayment(AccountsController): msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) frappe.throw(msg) - if self.payment_type == "Loan Closure" and flt(self.amount_paid, 2) < flt(self.payable_amount, 2): + if self.payment_type == "Loan Closure" and flt(self.amount_paid, precision) < flt(self.payable_amount, precision): msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) frappe.throw(msg) def update_paid_amount(self): + precision = cint(frappe.db.get_default("currency_precision")) or 2 + loan = frappe.get_doc("Loan", self.against_loan) for payment in self.repayment_details: @@ -75,9 +82,9 @@ class LoanRepayment(AccountsController): 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, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual)) - if flt(loan.total_principal_paid + self.principal_amount_paid, 2) >= flt(loan.total_payment, 2): + if flt(loan.total_principal_paid + self.principal_amount_paid, precision) >= flt(loan.total_payment, precision): if loan.is_secured_loan: frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") else: @@ -109,7 +116,7 @@ class LoanRepayment(AccountsController): def allocate_amounts(self, paid_entries): self.set('repayment_details', []) self.principal_amount_paid = 0 - interest_paid = 0 + interest_paid = self.amount_paid - self.penalty_amount if self.amount_paid - self.penalty_amount > 0 and paid_entries: interest_paid = self.amount_paid - self.penalty_amount @@ -253,6 +260,7 @@ def get_accrued_interest_entries(against_loan): # So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable def get_amounts(amounts, against_loan, posting_date, payment_type): + precision = cint(frappe.db.get_default("currency_precision")) or 2 against_loan_doc = frappe.get_doc("Loan", against_loan) loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type) @@ -282,8 +290,8 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): payable_principal_amount += entry.payable_principal_amount pending_accrual_entries.setdefault(entry.name, { - 'interest_amount': flt(entry.interest_amount), - 'payable_principal_amount': flt(entry.payable_principal_amount) + 'interest_amount': flt(entry.interest_amount, precision), + 'payable_principal_amount': flt(entry.payable_principal_amount, precision) }) if not final_due_date: @@ -301,11 +309,11 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365 total_pending_interest += (pending_days * per_day_interest) - amounts["pending_principal_amount"] = pending_principal_amount - amounts["payable_principal_amount"] = payable_principal_amount - amounts["interest_amount"] = total_pending_interest - amounts["penalty_amount"] = penalty_amount - amounts["payable_amount"] = payable_principal_amount + total_pending_interest + penalty_amount + 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, precision) + amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) amounts["pending_accrual_entries"] = pending_accrual_entries if final_due_date: diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json index 1553844704..4572e99299 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "LS-.{applicant}.-.#####", "creation": "2019-08-29 18:48:51.371674", "doctype": "DocType", @@ -6,10 +7,10 @@ "engine": "InnoDB", "field_order": [ "loan_details_section", - "loan_application", - "loan", "applicant_type", "applicant", + "loan", + "loan_application", "column_break_3", "company", "pledge_time", @@ -55,15 +56,13 @@ "fieldname": "loan", "fieldtype": "Link", "label": "Loan", - "options": "Loan", - "read_only": 1 + "options": "Loan" }, { "fieldname": "loan_application", "fieldtype": "Link", "label": "Loan Application", - "options": "Loan Application", - "read_only": 1 + "options": "Loan Application" }, { "fieldname": "total_security_value", @@ -133,7 +132,8 @@ } ], "is_submittable": 1, - "modified": "2019-10-10 13:22:53.297519", + "links": [], + "modified": "2020-07-02 23:38:24.002382", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Pledge", diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json index db260a4a9e..a55b482bd6 100644 --- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "LM-LSP-.####", "creation": "2019-09-03 18:20:31.382887", "doctype": "DocType", @@ -46,6 +47,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Loan Security Price", + "options": "Company:company:default_currency", "reqd": 1 }, { @@ -79,7 +81,8 @@ "read_only": 1 } ], - "modified": "2019-10-26 09:46:46.069667", + "links": [], + "modified": "2020-06-11 03:41:33.900340", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Price", 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 308c4385d3..ffd96737a5 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 @@ -19,7 +19,9 @@ def update_shortfall_status(loan, security_value): return if security_value >= loan_security_shortfall.shortfall_amount: - frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, "status", "Completed") + frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, { + "status": "Completed", + "shortfall_value": loan_security_shortfall.shortfall_amount}) else: frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, "shortfall_amount", loan_security_shortfall.shortfall_amount - security_value) diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 90ae3b01bd..669490a448 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -41,6 +41,7 @@ "options": "Company:company:default_currency" }, { + "default": "0", "fieldname": "rate_of_interest", "fieldtype": "Percent", "label": "Rate of Interest (%) Yearly", @@ -148,7 +149,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-05-16 09:08:09.029921", + "modified": "2020-06-07 18:55:59.346292", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", diff --git a/erpnext/loan_management/loan_common.js b/erpnext/loan_management/loan_common.js index 3a47a88cbe..d9dd415296 100644 --- a/erpnext/loan_management/loan_common.js +++ b/erpnext/loan_management/loan_common.js @@ -9,12 +9,15 @@ frappe.ui.form.on(cur_frm.doctype, { } if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual'].includes(frm.doc.doctype) - && frm.doc.docstatus == 1) { + && frm.doc.docstatus > 0) { frm.add_custom_button(__("Accounting Ledger"), function() { frappe.route_options = { voucher_no: frm.doc.name, - company: frm.doc.company + company: frm.doc.company, + from_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), + show_cancelled_entries: frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); 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 ea6a2ee645..1951855475 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 @@ -76,7 +76,8 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "currency", "options": "Currency", - "width": 50 + "width": 50, + "hidden": 1 } ] @@ -84,17 +85,13 @@ def get_columns(filters): def get_data(filters): - loan_security_price_map = frappe._dict(frappe.get_all("Loan Security", - fields=["name", "loan_security_price"], as_list=1 - )) - data = [] conditions = get_conditions(filters) 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, c.qty, c.loan_security_price, c.amount FROM `tabLoan Security Pledge` p, `tabPledge` c WHERE @@ -115,8 +112,8 @@ def get_data(filters): row["pledge_time"] = pledge.pledge_time row["loan_security"] = pledge.loan_security row["qty"] = pledge.qty - row["loan_security_price"] = loan_security_price_map.get(pledge.loan_security) - row["loan_security_value"] = row["loan_security_price"] * pledge.qty + row["loan_security_price"] = pledge.loan_security_price + row["loan_security_value"] = pledge.amount row["currency"] = default_currency data.append(row) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index b58f999cfb..add7bbfa57 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -44,7 +44,7 @@ class MaintenanceSchedule(TransactionBase): for d in self.get('items'): if d.serial_no: serial_nos = get_valid_serial_nos(d.serial_no) - self.validate_serial_no(serial_nos, d.start_date) + self.validate_serial_no(d.item_code, serial_nos, d.start_date) self.update_amc_date(serial_nos, d.end_date) no_email_sp = [] @@ -178,14 +178,18 @@ class MaintenanceSchedule(TransactionBase): serial_no_doc.amc_expiry_date = amc_expiry_date serial_no_doc.save() - def validate_serial_no(self, serial_nos, amc_start_date): + 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"], as_dict=1) + ["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") + 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}") diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json index c797b7ea77..11925681df 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json @@ -701,7 +701,7 @@ "columns": 0, "default": "Draft", "fieldname": "status", - "fieldtype": "Data", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -1001,7 +1001,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 14:44:44.911402", + "modified": "2020-07-15 14:44:44.911402", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit", diff --git a/erpnext/manufacturing/dashboard_fixtures.py b/erpnext/manufacturing/dashboard_fixtures.py new file mode 100644 index 0000000000..0e9a21c026 --- /dev/null +++ b/erpnext/manufacturing/dashboard_fixtures.py @@ -0,0 +1,241 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe, erpnext, json +from frappe import _ +from frappe.utils import nowdate, get_first_day, get_last_day, add_months + +def get_data(): + 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"} + ] + }] + +def get_charts(): + company = erpnext.get_default_company() + + if not company: + company = frappe.db.get_value("Company", {"is_group": 0}, "name") + + return [{ + "doctype": "Dashboard Chart", + "based_on": "modified", + "time_interval": "Yearly", + "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", + "time_interval": "Yearly", + "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" + }] \ No newline at end of file diff --git a/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json b/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json index 18604e283a..8d11294164 100644 --- a/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json @@ -3,7 +3,7 @@ { "hidden": 0, "label": "Production", - "links": "[\n {\n \"dependencies\": [\n \"Item\",\n \"BOM\"\n ],\n \"description\": \"Orders released for production.\",\n \"label\": \"Work Order\",\n \"name\": \"Work Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"BOM\"\n ],\n \"description\": \"Generate Material Requests (MRP) and Work Orders.\",\n \"label\": \"Production Plan\",\n \"name\": \"Production Plan\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Activity Type\"\n ],\n \"description\": \"Time Sheet for manufacturing.\",\n \"label\": \"Timesheet\",\n \"name\": \"Timesheet\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Job Card\",\n \"name\": \"Job Card\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\",\n \"BOM\"\n ],\n \"description\": \"Orders released for production.\",\n \"label\": \"Work Order\",\n \"name\": \"Work Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"BOM\"\n ],\n \"description\": \"Generate Material Requests (MRP) and Work Orders.\",\n \"label\": \"Production Plan\",\n \"name\": \"Production Plan\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Job Card\",\n \"name\": \"Job Card\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Downtime Entry\",\n \"name\": \"Downtime Entry\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -13,7 +13,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Open Work Orders\",\n \"name\": \"Open Work Orders\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Work Orders in Progress\",\n \"name\": \"Work Orders in Progress\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Issued Items Against Work Order\",\n \"name\": \"Issued Items Against Work Order\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Completed Work Orders\",\n \"name\": \"Completed Work Orders\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Work Order\"\n ],\n \"doctype\": \"Work Order\",\n \"is_query_report\": true,\n \"label\": \"Production Analytics\",\n \"name\": \"Production Analytics\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"BOM\"\n ],\n \"doctype\": \"BOM\",\n \"is_query_report\": true,\n \"label\": \"BOM Search\",\n \"name\": \"BOM Search\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"BOM\"\n ],\n \"doctype\": \"BOM\",\n \"is_query_report\": true,\n \"label\": \"BOM Stock Report\",\n \"name\": \"BOM Stock Report\",\n \"type\": \"report\"\n }\n]" + "links": "[{\n\t\"dependencies\": [\"Work Order\"],\n\t\"name\": \"Production Planning Report\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Work Order\",\n\t\"label\": \"Production Planning Report\"\n}, {\n\t\"dependencies\": [\"Work Order\"],\n\t\"name\": \"Work Order Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Work Order\",\n\t\"label\": \"Work Order Summary\"\n}, {\n\t\"dependencies\": [\"Quality Inspection\"],\n\t\"name\": \"Quality Inspection Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Quality Inspection\",\n\t\"label\": \"Quality Inspection Summary\"\n}, {\n\t\"dependencies\": [\"Downtime Entry\"],\n\t\"name\": \"Downtime Analysis\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Downtime Entry\",\n\t\"label\": \"Downtime Analysis\"\n}, {\n\t\"dependencies\": [\"Job Card\"],\n\t\"name\": \"Job Card Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Job Card\",\n\t\"label\": \"Job Card Summary\"\n}, {\n\t\"dependencies\": [\"BOM\"],\n\t\"name\": \"BOM Search\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"BOM\",\n\t\"label\": \"BOM Search\"\n}, {\n\t\"dependencies\": [\"BOM\"],\n\t\"name\": \"BOM Stock Report\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"BOM\",\n\t\"label\": \"BOM Stock Report\"\n}, {\n\t\"dependencies\": [\"Work Order\"],\n\t\"name\": \"Production Analytics\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Work Order\",\n\t\"label\": \"Production Analytics\"\n}, {\n\t\"dependencies\": [\"BOM\"],\n\t\"name\": \"BOM Operations Time\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"BOM\",\n\t\"label\": \"BOM Operations Time\"\n}]" }, { "hidden": 0, @@ -32,23 +32,93 @@ } ], "category": "Domains", - "charts": [], + "charts": [ + { + "chart_name": "Produced Quantity" + } + ], "creation": "2020-03-02 17:11:37.032604", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Manufacturing", - "modified": "2020-04-01 11:28:50.979358", + "modified": "2020-05-28 13:54:02.048419", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", + "onboarding": "Manufacturing", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "restrict_to_domain": "Manufacturing", - "shortcuts": [] + "shortcuts": [ + { + "color": "#cef6d1", + "format": "{} Active", + "label": "Item", + "link_to": "Item", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{\n \"disabled\": 0\n}", + "type": "DocType" + }, + { + "color": "#cef6d1", + "format": "{} Active", + "label": "BOM", + "link_to": "BOM", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{\n \"is_active\": 1\n}", + "type": "DocType" + }, + { + "color": "#ffe8cd", + "format": "{} Open", + "label": "Work Order", + "link_to": "Work Order", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{ \n \"status\": [\"in\", \n [\"Draft\", \"Not Started\", \"In Process\"]\n ]\n}", + "type": "DocType" + }, + { + "color": "#ffe8cd", + "format": "{} Open", + "label": "Production Plan", + "link_to": "Production Plan", + "restrict_to_domain": "Manufacturing", + "stats_filter": "{ \n \"status\": [\"not in\", [\"Completed\"]]\n}", + "type": "DocType" + }, + { + "label": "Forecasting", + "link_to": "Exponential Smoothing Forecasting", + "type": "Report" + }, + { + "label": "Work Order Summary", + "link_to": "Work Order Summary", + "restrict_to_domain": "Manufacturing", + "type": "Report" + }, + { + "label": "BOM Stock Report", + "link_to": "BOM Stock Report", + "type": "Report" + }, + { + "label": "Production Planning Report", + "link_to": "Production Planning Report", + "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "Manufacturing", + "restrict_to_domain": "Manufacturing", + "type": "Dashboard" + } + ] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index ebfb762640..47b4207241 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -29,10 +29,7 @@ frappe.ui.form.on("BOM", { frm.set_query("item", function() { return { - query: "erpnext.controllers.queries.item_query", - filters: { - "doctype": "BOM" - } + query: "erpnext.manufacturing.doctype.bom.bom.item_query" }; }); @@ -44,9 +41,12 @@ frappe.ui.form.on("BOM", { }; }); - frm.set_query("item_code", "items", function() { + frm.set_query("item_code", "items", function(doc) { return { - query: "erpnext.controllers.queries.item_query" + query: "erpnext.manufacturing.doctype.bom.bom.item_query", + filters: { + "item_code": doc.item + } }; }); @@ -96,6 +96,12 @@ frappe.ui.form.on("BOM", { frm.trigger("make_work_order"); }, __("Create")); + if (frm.doc.has_variants) { + frm.add_custom_button(__("Variant BOM"), function() { + frm.trigger("make_variant_bom"); + }, __("Create")); + } + if (frm.doc.inspection_required) { frm.add_custom_button(__("Quality Inspection"), function() { frm.trigger("make_quality_inspection"); @@ -124,7 +130,7 @@ frappe.ui.form.on("BOM", { } - if (frm.doc.__onload && frm.doc.__onload["has_variants"]) { + if (frm.doc.has_variants) { frm.set_intro(__('This is a Template BOM and will be used to make the work order for {0} of the item {1}', [ `variants`, @@ -138,9 +144,52 @@ frappe.ui.form.on("BOM", { }, make_work_order: function(frm) { + frm.events.setup_variant_prompt(frm, "Work Order", (frm, item, data, variant_items) => { + frappe.call({ + method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", + args: { + bom_no: frm.doc.name, + item: item, + qty: data.qty || 0.0, + project: frm.doc.project, + variant_items: variant_items + }, + freeze: true, + callback: function(r) { + if(r.message) { + let doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + }); + }, + + make_variant_bom: function(frm) { + frm.events.setup_variant_prompt(frm, "Variant BOM", (frm, item, data, variant_items) => { + frappe.call({ + method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom", + args: { + source_name: frm.doc.name, + bom_no: frm.doc.name, + item: item, + variant_items: variant_items + }, + freeze: true, + callback: function(r) { + if(r.message) { + let doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + }, true); + }, + + setup_variant_prompt: function(frm, title, callback, skip_qty_field) { const fields = []; - if (frm.doc.__onload && frm.doc.__onload["has_variants"]) { + if (frm.doc.has_variants) { fields.push({ fieldtype: 'Link', label: __('Variant Item'), @@ -158,34 +207,106 @@ frappe.ui.form.on("BOM", { }); } - fields.push({ - fieldtype: 'Float', - label: __('Qty To Manufacture'), - fieldname: 'qty', - reqd: 1, - default: 1 + if (!skip_qty_field) { + fields.push({ + fieldtype: 'Float', + label: __('Qty To Manufacture'), + fieldname: 'qty', + reqd: 1, + default: 1 + }); + } + + var has_template_rm = frm.doc.items.filter(d => d.has_variants === 1) || []; + if (has_template_rm && has_template_rm.length > 0) { + fields.push({ + fieldname: "items", + fieldtype: "Table", + label: __("Raw Materials"), + fields: [ + { + fieldname: "item_code", + options: "Item", + label: __("Template Item"), + fieldtype: "Link", + in_list_view: 1, + reqd: 1, + }, + { + fieldname: "varint_item_code", + options: "Item", + label: __("Variant Item"), + fieldtype: "Link", + in_list_view: 1, + reqd: 1, + get_query: function(data) { + if (!data.item_code) { + frappe.throw(__("Select template item")); + } + + return { + query: "erpnext.controllers.queries.item_query", + filters: { + "variant_of": data.item_code + } + }; + } + }, + { + fieldname: "qty", + label: __("Quantity"), + fieldtype: "Float", + in_list_view: 1, + reqd: 1, + }, + { + fieldname: "source_warehouse", + label: __("Source Warehouse"), + fieldtype: "Link", + options: "Warehouse" + }, + { + fieldname: "operation", + label: __("Operation"), + fieldtype: "Data", + hidden: 1, + } + ], + in_place_edit: true, + data: [], + get_data: function () { + return []; + }, + }); + } + + let dialog = frappe.prompt(fields, data => { + let item = data.item || frm.doc.item; + let variant_items = data.items || []; + + variant_items.forEach(d => { + if (!d.varint_item_code) { + frappe.throw(__("Select variant item code for the template item {0}", [d.item_code])); + } + }) + + callback(frm, item, data, variant_items); + + }, __(title), __("Create")); + + has_template_rm.forEach(d => { + dialog.fields_dict.items.df.data.push({ + "item_code": d.item_code, + "varint_item_code": "", + "qty": d.qty, + "source_warehouse": d.source_warehouse, + "operation": d.operation + }); }); - frappe.prompt(fields, data => { - let item = data.item || frm.doc.item; - - frappe.call({ - method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", - args: { - bom_no: frm.doc.name, - item: item, - qty: data.qty || 0.0, - project: frm.doc.project - }, - freeze: true, - callback: function(r) { - if(r.message) { - var doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name); - } - } - }); - }, __("Enter Value"), __("Create")); + if (has_template_rm) { + dialog.fields_dict.items.grid.refresh(); + } }, make_quality_inspection: function(frm) { @@ -212,6 +333,12 @@ frappe.ui.form.on("BOM", { }); }, + rm_cost_as_per: function(frm) { + if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) { + frm.set_value("plc_conversion_rate", 1.0); + } + }, + routing: function(frm) { if (frm.doc.routing) { frappe.call({ @@ -242,7 +369,7 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({ item_code: function(doc, cdt, cdn){ var scrap_items = false; var child = locals[cdt][cdn]; - if(child.doctype == 'BOM Scrap Item') { + if (child.doctype == 'BOM Scrap Item') { scrap_items = true; } @@ -252,8 +379,19 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({ get_bom_material_detail(doc, cdt, cdn, scrap_items); }, + + buying_price_list: function(doc) { + this.apply_price_list(); + }, + + plc_conversion_rate: function(doc) { + if (!this.in_apply_price_list) { + this.apply_price_list(null, true); + } + }, + conversion_factor: function(doc, cdt, cdn) { - if(frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { + if (frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { var item = frappe.get_doc(cdt, cdn); frappe.model.round_floats_in(item, ["qty", "conversion_factor"]); item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item)); diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 63f4f977c5..f551b91597 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "creation": "2013-01-22 15:11:38", "doctype": "DocType", @@ -6,23 +7,25 @@ "engine": "InnoDB", "field_order": [ "item", - "quantity", - "set_rate_of_sub_assembly_item_based_on_bom", + "company", + "item_name", + "uom", "cb0", "is_active", "is_default", "allow_alternative_item", - "image", - "item_name", - "uom", - "currency_detail", - "company", + "set_rate_of_sub_assembly_item_based_on_bom", "project", + "quantity", + "image", + "currency_detail", + "currency", "conversion_rate", "column_break_12", - "currency", "rm_cost_as_per", "buying_price_list", + "price_list_currency", + "plc_conversion_rate", "section_break_21", "with_operations", "column_break_23", @@ -50,6 +53,7 @@ "section_break_25", "description", "column_break_27", + "has_variants", "section_break0", "exploded_items", "website_section", @@ -176,7 +180,8 @@ }, { "fieldname": "currency_detail", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Currency and Price List" }, { "fieldname": "company", @@ -324,7 +329,7 @@ }, { "fieldname": "base_scrap_material_cost", - "fieldtype": "Data", + "fieldtype": "Currency", "label": "Scrap Material Cost(Company Currency)", "no_copy": 1, "options": "Company:company:default_currency", @@ -477,13 +482,42 @@ { "fieldname": "column_break_52", "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "plc_conversion_rate", + "fieldtype": "Float", + "label": "Price List Exchange Rate" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "price_list_currency", + "fieldtype": "Link", + "label": "Price List Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "item.has_variants", + "fieldname": "has_variants", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Has Variants", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-sitemap", "idx": 1, "image_field": "image", "is_submittable": 1, - "modified": "2019-11-22 14:35:12.142150", + "links": [], + "modified": "2020-05-21 12:29:32.634952", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index b1fc4deae9..8062342cfc 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -3,13 +3,16 @@ from __future__ import unicode_literals import frappe, erpnext -from frappe.utils import cint, cstr, flt +from frappe.utils import cint, cstr, flt, today from frappe import _ from erpnext.setup.utils import get_exchange_rate from frappe.website.website_generator import WebsiteGenerator from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_price_list_rate from frappe.core.doctype.version.version import get_diff +from erpnext.controllers.queries import get_match_cond +from erpnext.stock.doctype.item.item import get_item_details +from frappe.model.mapper import get_mapped_doc import functools @@ -59,22 +62,19 @@ class BOM(WebsiteGenerator): self.name = name - def onload(self): - super(BOM, self).onload() - if self.get("item") and cint(frappe.db.get_value("Item", self.item, "has_variants")): - self.set_onload("has_variants", True) - def validate(self): self.route = frappe.scrub(self.name).replace('_', '-') self.clear_operations() self.validate_main_item() self.validate_currency() self.set_conversion_rate() + self.set_plc_conversion_rate() self.validate_uom_is_interger() self.set_bom_material_details() self.validate_materials() self.validate_operations() self.calculate_cost() + self.update_cost(update_parent=False, from_child_bom=True, save=False) def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -101,9 +101,7 @@ class BOM(WebsiteGenerator): self.manage_default_bom() def get_item_det(self, item_code): - item = frappe.db.sql("""select name, item_name, docstatus, description, image, - is_sub_contracted_item, stock_uom, default_bom, last_purchase_rate, include_item_in_manufacturing - from `tabItem` where name=%s""", item_code, as_dict = 1) + item = get_item_details(item_code) if not item: frappe.throw(_("Item: {0} does not exist in the system").format(item_code)) @@ -114,8 +112,16 @@ class BOM(WebsiteGenerator): if self.routing: self.set("operations", []) for d in frappe.get_all("BOM Operation", fields = ["*"], - filters = {'parenttype': 'Routing', 'parent': self.routing}): - child = self.append('operations', d) + filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="idx"): + child = self.append('operations', { + "operation": d.operation, + "workstation": d.workstation, + "description": d.description, + "time_in_mins": d.time_in_mins, + "batch_size": d.batch_size, + "operating_cost": d.operating_cost, + "idx": d.idx + }) child.hour_rate = flt(d.hour_rate / self.conversion_rate, 2) def set_bom_material_details(self): @@ -148,10 +154,10 @@ class BOM(WebsiteGenerator): item = self.get_item_det(args['item_code']) - args['bom_no'] = args['bom_no'] or item and cstr(item[0]['default_bom']) or '' + 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[0].include_item_in_manufacturing or 0) - args.update(item[0]) + item and item.include_item_in_manufacturing or 0) + args.update(item) rate = self.get_rm_rate(args) ret_item = { @@ -165,7 +171,7 @@ class BOM(WebsiteGenerator): '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' : rate, + 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1), 'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0 } @@ -183,40 +189,14 @@ class BOM(WebsiteGenerator): self.rm_cost_as_per = "Valuation Rate" if arg.get('scrap_items'): - rate = self.get_valuation_rate(arg) + rate = get_valuation_rate(arg) elif arg: #Customer Provided parts will have zero rate if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item'): 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: - if self.rm_cost_as_per == 'Valuation Rate': - rate = self.get_valuation_rate(arg) * (arg.get("conversion_factor") or 1) - elif self.rm_cost_as_per == 'Last Purchase Rate': - rate = flt(arg.get('last_purchase_rate') \ - or frappe.db.get_value("Item", arg['item_code'], "last_purchase_rate")) \ - * (arg.get("conversion_factor") or 1) - elif self.rm_cost_as_per == "Price List": - if not self.buying_price_list: - frappe.throw(_("Please select Price List")) - args = frappe._dict({ - "doctype": "BOM", - "price_list": self.buying_price_list, - "qty": arg.get("qty") or 1, - "uom": arg.get("uom") or arg.get("stock_uom"), - "stock_uom": arg.get("stock_uom"), - "transaction_type": "buying", - "company": self.company, - "currency": self.currency, - "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function - "conversion_factor": arg.get("conversion_factor") or 1, - "plc_conversion_rate": 1, - "ignore_party": True - }) - item_doc = frappe.get_doc("Item", arg.get("item_code")) - out = frappe._dict() - get_price_list_rate(args, item_doc, out) - rate = out.price_list_rate + rate = get_bom_item_rate(arg, self) if not rate: if self.rm_cost_as_per == "Price List": @@ -226,7 +206,7 @@ class BOM(WebsiteGenerator): frappe.msgprint(_("{0} not found for item {1}") .format(self.rm_cost_as_per, arg["item_code"]), alert=True) - return flt(rate) / (self.conversion_rate or 1) + return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) def update_cost(self, update_parent=True, from_child_bom=False, save=True): if self.docstatus == 2: @@ -243,10 +223,15 @@ class BOM(WebsiteGenerator): "stock_uom": d.stock_uom, "conversion_factor": d.conversion_factor }) + if rate: d.rate = rate d.amount = flt(d.rate) * flt(d.qty) - d.db_update() + d.base_rate = flt(d.rate) * flt(self.conversion_rate) + d.base_amount = flt(d.amount) * flt(self.conversion_rate) + + if save: + d.db_update() if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True @@ -279,31 +264,6 @@ class BOM(WebsiteGenerator): where is_active = 1 and name = %s""", bom_no, as_dict=1) return bom and bom[0]['unit_cost'] or 0 - def get_valuation_rate(self, args): - """ Get weighted average of valuation rate from all warehouses """ - - total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 - for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin` - where item_code=%s""", args['item_code'], as_dict=1): - total_qty += flt(d.actual_qty) - total_value += flt(d.stock_value) - - if total_qty: - valuation_rate = total_value / total_qty - - if valuation_rate <= 0: - last_valuation_rate = frappe.db.sql("""select valuation_rate - from `tabStock Ledger Entry` - where item_code = %s and valuation_rate > 0 - 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") - - return flt(valuation_rate) - 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, @@ -372,6 +332,13 @@ class BOM(WebsiteGenerator): 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") + 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") + def validate_materials(self): """ Validate raw material entries """ @@ -527,7 +494,7 @@ class BOM(WebsiteGenerator): 'image' : d.image, 'stock_uom' : d.stock_uom, 'stock_qty' : flt(d.stock_qty), - 'rate' : d.base_rate, + 'rate' : flt(d.base_rate) / flt(d.conversion_factor), 'include_item_in_manufacturing': d.include_item_in_manufacturing })) @@ -610,6 +577,62 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 +def get_bom_item_rate(args, bom_doc): + if bom_doc.rm_cost_as_per == 'Valuation Rate': + rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) + 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 == "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 + }) + item_doc = frappe.get_cached_doc("Item", args.get("item_code")) + out = frappe._dict() + get_price_list_rate(bom_args, item_doc, out) + rate = out.price_list_rate + + return rate + +def get_valuation_rate(args): + """ Get weighted average of valuation rate from all warehouses """ + + total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 + for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin` + where item_code=%s""", args['item_code'], as_dict=1): + total_qty += flt(d.actual_qty) + total_value += flt(d.stock_value) + + if total_qty: + valuation_rate = total_value / total_qty + + if valuation_rate <= 0: + last_valuation_rate = frappe.db.sql("""select valuation_rate + from `tabStock Ledger Entry` + where item_code = %s and valuation_rate > 0 + 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") + + return flt(valuation_rate) + def get_list_context(context): context.title = _("Bill of Materials") # context.introduction = _('Boms') @@ -625,6 +648,8 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * %(qty)s as qty, item.image, bom.project, + bom_item.rate, + bom_item.amount, item.stock_uom, item.item_group, item.allow_alternative_item, @@ -641,6 +666,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite where bom_item.docstatus < 2 and bom.name = %(bom)s + and ifnull(item.has_variants, 0) = 0 and item.is_stock_item in (1, {is_stock_item}) {where_conditions} group by item_code, stock_uom @@ -883,3 +909,85 @@ def get_bom_diff(bom1, bom2): out.removed.append([df.fieldname, d.as_dict()]) return out + +@frappe.whitelist() +def item_query(doctype, txt, searchfield, start, page_len, filters): + meta = frappe.get_meta("Item", cached=True) + searchfields = meta.get_search_fields() + + 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"]]) + + 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()) + } + + 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", + fields=["distinct parent as item_code"], + filters = {"barcode": ("like", "%{0}%".format(txt))}) + + barcodes = [d.item_code for d in barcodes] + if barcodes: + or_cond_filters["name"] = ("in", barcodes) + + for cond in get_match_cond(doctype, as_condition=False): + for key, value in cond.items(): + if key == doctype: + key = "name" + + query_filters[key] = ("in", value) + + if filters and filters.get("item_code"): + has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants") + if not has_variants: + query_filters["has_variants"] = 0 + + return frappe.get_all("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): + from erpnext.manufacturing.doctype.work_order.work_order import add_variant_item + + def postprocess(source, doc): + doc.item = item + 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 + }) + + add_variant_item(variant_items, doc, source_name) + + doc = get_mapped_doc('BOM', source_name, { + 'BOM': { + 'doctype': 'BOM', + 'validation': { + 'docstatus': ['=', 1] + } + }, + 'BOM Item': { + 'doctype': 'BOM Item', + 'condition': lambda doc: doc.has_variants == 0 + }, + }, target_doc, postprocess) + + return doc diff --git a/erpnext/manufacturing/doctype/bom/bom_list.js b/erpnext/manufacturing/doctype/bom/bom_list.js index 2b06ed72ed..94cb466bd8 100644 --- a/erpnext/manufacturing/doctype/bom/bom_list.js +++ b/erpnext/manufacturing/doctype/bom/bom_list.js @@ -1,7 +1,9 @@ frappe.listview_settings['BOM'] = { - add_fields: ["is_active", "is_default", "total_cost"], + add_fields: ["is_active", "is_default", "total_cost", "has_variants"], get_indicator: function(doc) { - if(doc.is_default) { + if(doc.is_active && doc.has_variants) { + return [__("Template"), "orange", "has_variants,=,Yes"]; + } else if(doc.is_default) { return [__("Default"), "green", "is_default,=,Yes"]; } else if(doc.is_active) { return [__("Active"), "blue", "is_active,=,Yes"]; diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 45a7b935d3..3dfd03b139 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -81,13 +81,13 @@ class TestBOM(unittest.TestCase): # test amounts in selected currency self.assertEqual(bom.operating_cost, 100) - self.assertEqual(bom.raw_material_cost, 8000) - self.assertEqual(bom.total_cost, 8100) + self.assertEqual(bom.raw_material_cost, 351.68) + self.assertEqual(bom.total_cost, 451.68) # test amounts in selected currency self.assertEqual(bom.base_operating_cost, 6000) - self.assertEqual(bom.base_raw_material_cost, 480000) - self.assertEqual(bom.base_total_cost, 486000) + self.assertEqual(bom.base_raw_material_cost, 21100.80) + self.assertEqual(bom.base_total_cost, 27100.80) 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) diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index f094be4c64..e34be61bc7 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -1,8 +1,10 @@ { + "actions": [], "creation": "2013-02-22 01:27:49", "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "item_code", "item_name", @@ -33,6 +35,7 @@ "scrap", "qty_consumed_per_unit", "section_break_27", + "has_variants", "include_item_in_manufacturing", "original_item" ], @@ -57,6 +60,7 @@ "label": "Item Name" }, { + "depends_on": "eval:parent.with_operations == 1", "fieldname": "operation", "fieldtype": "Link", "label": "Item operation", @@ -258,11 +262,22 @@ "label": "Original Item", "options": "Item", "read_only": 1 + }, + { + "default": "0", + "fetch_from": "item_code.has_variants", + "fieldname": "has_variants", + "fieldtype": "Check", + "label": "Has Variants", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2019-11-22 11:38:52.087303", + "links": [], + "modified": "2020-04-09 14:30:26.535546", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 3ca851d783..0350e2cb37 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -78,6 +78,7 @@ "read_only": 1 }, { + "depends_on": "eval:parent.doctype == 'BOM'", "fieldname": "base_hour_rate", "fieldtype": "Currency", "label": "Base Hour Rate(Company Currency)", @@ -87,6 +88,7 @@ }, { "default": "5", + "depends_on": "eval:parent.doctype == 'BOM'", "fieldname": "base_operating_cost", "fieldtype": "Currency", "label": "Operating Cost(Company Currency)", @@ -108,12 +110,12 @@ ], "idx": 1, "istable": 1, - "modified": "2019-07-16 22:35:55.374037", - "modified_by": "govindsmenokee@gmail.com", + "modified": "2020-06-16 17:01:11.128420", + "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_other_income/__init__.py b/erpnext/manufacturing/doctype/downtime_entry/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_other_income/__init__.py rename to erpnext/manufacturing/doctype/downtime_entry/__init__.py diff --git a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js new file mode 100644 index 0000000000..3b7f5ba8d7 --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Downtime Entry', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.json b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.json new file mode 100644 index 0000000000..b301a9ec05 --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.json @@ -0,0 +1,141 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2020-04-18 04:50:46.187638", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "workstation", + "operator", + "column_break_4", + "from_time", + "to_time", + "downtime", + "downtime_reason_section", + "stop_reason", + "column_break_9", + "remarks" + ], + "fields": [ + { + "fieldname": "workstation", + "fieldtype": "Link", + "label": "Workstation / Machine", + "options": "Workstation", + "reqd": 1 + }, + { + "fieldname": "from_time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "From Time", + "reqd": 1 + }, + { + "fieldname": "to_time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "To Time", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "operator", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operator", + "options": "Employee", + "reqd": 1 + }, + { + "fieldname": "downtime_reason_section", + "fieldtype": "Section Break", + "label": "Downtime Reason" + }, + { + "description": "In Mins", + "fieldname": "downtime", + "fieldtype": "Float", + "label": "Downtime", + "read_only": 1 + }, + { + "fieldname": "stop_reason", + "fieldtype": "Select", + "label": "Stop Reason", + "options": "\nExcessive machine set up time\nUnplanned machine maintenance\nOn-machine press checks\nMachine operator errors\nMachine malfunction\nElectricity down\nOther", + "reqd": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "remarks", + "fieldtype": "Text", + "label": "Remarks" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "DT-", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-05-26 22:14:54.479831", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Downtime Entry", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "workstation", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.py b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.py new file mode 100644 index 0000000000..56ec4356af --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/downtime_entry.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import time_diff_in_hours +from frappe.model.document import Document + +class DowntimeEntry(Document): + def validate(self): + if self.from_time and self.to_time: + self.downtime = time_diff_in_hours(self.to_time, self.from_time) * 60 diff --git a/erpnext/manufacturing/doctype/downtime_entry/test_downtime_entry.py b/erpnext/manufacturing/doctype/downtime_entry/test_downtime_entry.py new file mode 100644 index 0000000000..8b2a8d36c1 --- /dev/null +++ b/erpnext/manufacturing/doctype/downtime_entry/test_downtime_entry.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestDowntimeEntry(unittest.TestCase): + pass diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 7661fffa86..fba670c1c1 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2018-07-09 17:23:29.518745", "doctype": "DocType", @@ -264,8 +265,10 @@ { "fetch_from": "work_order.production_item", "fieldname": "production_item", - "fieldtype": "Read Only", - "label": "Production Item" + "fieldtype": "Link", + "label": "Production Item", + "options": "Item", + "read_only": 1 }, { "fieldname": "barcode", @@ -274,7 +277,8 @@ "read_only": 1 }, { - "fetch_from": "work_order.item_name", + "fetch_from": "production_item.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Read Only", "label": "Item Name" @@ -290,7 +294,8 @@ } ], "is_submittable": 1, - "modified": "2020-03-27 13:36:35.417502", + "links": [], + "modified": "2020-04-20 15:14:00.273441", "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 e43b98aee1..c29d4ba3d5 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -102,8 +102,11 @@ class JobCard(Document): 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"))): - row.remaining_time_in_mins -= time_diff_in_minutes(row.planned_end_time, - row.planned_start_time) + 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) self.update_time_logs(row) return diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js index ac144e24b1..668e981d18 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js @@ -3,3 +3,31 @@ frappe.ui.form.on('Manufacturing Settings', { }); + +frappe.tour["Manufacturing Settings"] = [ + { + fieldname: "material_consumption", + title: __("Allow Multiple Material Consumption"), + description: __("If ticked, multiple materials can be used for a single Work Order. This is useful if one or more time consuming products are being manufactured.") + }, + { + fieldname: "backflush_raw_materials_based_on", + title: __("Backflush Raw Materials"), + description: __("The Stock Entry of type 'Manufacture' is known as backflush. Raw materials being consumed to manufacture finished goods is known as backflushing.

When creating Manufacture Entry, raw-material items are backflushed based on BOM of production item. If you want raw-material items to be backflushed based on Material Transfer entry made against that Work Order instead, then you can set it under this field.") + }, + { + fieldname: "default_wip_warehouse", + title: __("Work In Progress Warehouse"), + description: __("This Warehouse will be auto-updated in the Work In Progress Warehouse field of Work Orders.") + }, + { + fieldname: "default_fg_warehouse", + title: __("Finished Goods Warehouse"), + description: __("This Warehouse will be auto-updated in the Target Warehouse field of Work Order.") + }, + { + fieldname: "update_bom_costs_automatically", + title: __("Update BOM Cost Automatically"), + description: __("If ticked, the BOM cost will be automatically updated based on Valuation Rate / Price List Rate / last purchase rate of raw materials.") + } +]; \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 64c952b67b..1a64bc5e24 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -201,9 +201,9 @@ frappe.ui.form.on('Production Plan', { title: title, fields: [ { - "fieldtype": "Table MultiSelect", "label": __("Source Warehouses"), + "fieldtype": "Table MultiSelect", "label": __("Source Warehouses (Optional)"), "fieldname": "warehouses", "options": "Production Plan Material Request Warehouse", - "description": "System will pickup the materials from the selected warehouses", + "description": __("System will pickup the materials from the selected warehouses. If not specified, system will create material request for purchase."), get_query: function () { return { filters: { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 560286e68a..c8892376b7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -98,11 +98,17 @@ class ProductionPlan(Document): elif self.get_items_from == "Material Request": self.get_mr_items() + def get_so_mr_list(self, field, table): + """Returns a list of Sales Orders or Material Requests from the respective tables""" + so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)] + return so_mr_list + def get_so_items(self): - so_list = [d.sales_order for d in self.sales_orders if d.sales_order] - if not so_list: - msgprint(_("Please enter Sales Orders in the above table")) - return [] + # Check for empty table or empty rows + if not self.get("sales_orders") or not self.get_so_mr_list("sales_order", "sales_orders"): + frappe.throw(_("Please fill the Sales Orders table"), title=_("Sales Orders Required")) + + so_list = self.get_so_mr_list("sales_order", "sales_orders") item_condition = "" if self.item_code: @@ -134,10 +140,11 @@ class ProductionPlan(Document): self.calculate_total_planned_qty() def get_mr_items(self): - mr_list = [d.material_request for d in self.material_requests if d.material_request] - if not mr_list: - msgprint(_("Please enter Material Requests in the above table")) - return [] + # 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")) + + mr_list = self.get_so_mr_list("material_request", "material_requests") item_condition = "" if self.item_code: @@ -628,16 +635,19 @@ def get_items_for_material_requests(doc, warehouses=None): if warehouse_list: warehouses = list(set(warehouse_list)) - + if doc.get("for_warehouse") and doc.get("for_warehouse") in warehouses: warehouses.remove(doc.get("for_warehouse")) warehouse_list = None doc['mr_items'] = [] + po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items') - if not po_items: - frappe.throw(_("Items are required to pull the raw materials which is associated with it.")) + # 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")) company = doc.get('company') ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty') diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js index 6cfd0bae5b..d7589fa390 100644 --- a/erpnext/manufacturing/doctype/routing/routing.js +++ b/erpnext/manufacturing/doctype/routing/routing.js @@ -44,7 +44,6 @@ frappe.ui.form.on('BOM Operation', { name: d.workstation }, callback: function (data) { - frappe.model.set_value(d.doctype, d.name, "base_hour_rate", data.message.hour_rate); frappe.model.set_value(d.doctype, d.name, "hour_rate", data.message.hour_rate); frm.events.calculate_operating_cost(frm, d); } diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index c125571960..a244f582c4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -449,6 +449,32 @@ frappe.ui.form.on("Work Order Item", { } }); } + }, + + item_code: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + + if (row.item_code) { + frappe.call({ + method: "erpnext.stock.doctype.item.item.get_item_details", + args: { + item_code: row.item_code, + company: frm.doc.company + }, + callback: function(r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, { + "required_qty": 1, + "item_name": r.message.item_name, + "description": r.message.description, + "source_warehouse": r.message.default_warehouse, + "allow_alternative_item": r.message.allow_alternative_item, + "include_item_in_manufacturing": r.message.include_item_in_manufacturing + }); + } + } + }); + } } }); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 00a67a03d6..585a09db2b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -38,11 +38,12 @@ "required_items", "time", "planned_start_date", - "actual_start_date", - "column_break_13", "planned_end_date", - "actual_end_date", "expected_delivery_date", + "column_break_13", + "actual_start_date", + "actual_end_date", + "lead_time", "operations_section", "transfer_material_against", "operations", @@ -108,6 +109,8 @@ }, { "depends_on": "eval:doc.production_item", + "fetch_from": "production_item.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Data", "label": "Item Name", @@ -281,27 +284,30 @@ "reqd": 1 }, { + "allow_on_submit": 1, "fieldname": "actual_start_date", "fieldtype": "Datetime", "label": "Actual Start Date", - "read_only": 1 + "read_only_depends_on": "eval:doc.operations && doc.operations.length > 0" }, { "fieldname": "column_break_13", "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "fieldname": "planned_end_date", "fieldtype": "Datetime", "label": "Planned End Date", "no_copy": 1, - "read_only": 1 + "read_only_depends_on": "eval:doc.operations && doc.operations.length > 0" }, { + "allow_on_submit": 1, "fieldname": "actual_end_date", "fieldtype": "Datetime", "label": "Actual End Date", - "read_only": 1 + "read_only_depends_on": "eval:doc.operations && doc.operations.length > 0" }, { "allow_on_submit": 1, @@ -476,6 +482,13 @@ "fieldtype": "Link", "label": "Source Warehouse", "options": "Warehouse" + }, + { + "description": "In Mins", + "fieldname": "lead_time", + "fieldtype": "Float", + "label": "Lead Time", + "read_only": 1 } ], "icon": "fa fa-cogs", @@ -483,7 +496,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-04-24 19:32:43.323054", + "modified": "2020-05-05 19:32:43.323054", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 8301f30d83..f962a1157b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -6,11 +6,11 @@ import frappe import json import math from frappe import _ -from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate, get_link_to_form +from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate, get_link_to_form, time_diff_in_hours from frappe.model.document import Document -from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict +from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict, get_bom_item_rate from dateutil.relativedelta import relativedelta -from erpnext.stock.doctype.item.item import validate_end_of_life +from erpnext.stock.doctype.item.item import validate_end_of_life, get_item_defaults from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError from erpnext.projects.doctype.timesheet.timesheet import OverlapError from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations @@ -279,7 +279,7 @@ class WorkOrder(Document): if enable_capacity_planning and job_card_doc: row.planned_start_time = job_card_doc.time_logs[-1].from_time row.planned_end_time = job_card_doc.time_logs[-1].to_time - print(row.planned_start_time, original_start_time, plan_days) + 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}.") @@ -437,8 +437,6 @@ class WorkOrder(Document): frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'")) def set_actual_dates(self): - self.actual_start_date = None - self.actual_end_date = None if self.get("operations"): actual_start_dates = [d.actual_start_time for d in self.get("operations") if d.actual_start_time] if actual_start_dates: @@ -447,6 +445,27 @@ class WorkOrder(Document): actual_end_dates = [d.actual_end_time for d in self.get("operations") if d.actual_end_time] 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 = { + "work_order": self.name, + "purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]) + } + ) + + if data and len(data): + dates = [d.posting_datetime for d in data] + self.actual_start_date = min(dates) + + if self.status == "Completed": + self.actual_end_date = max(dates) + + self.set_lead_time() + + def set_lead_time(self): + if self.actual_start_date and self.actual_end_date: + self.lead_time = flt(time_diff_in_hours(self.actual_end_date, self.actual_start_date) * 60) def delete_job_card(self): for d in frappe.get_all("Job Card", ["name"], {"work_order": self.name}): @@ -522,6 +541,8 @@ class WorkOrder(Document): # For instance in BOM Explosion Item child table, the items coming from sub assembly items for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999): self.append('required_items', { + 'rate': item.rate, + 'amount': item.amount, 'operation': item.operation, 'item_code': item.item_code, 'item_name': item.item_name, @@ -610,6 +631,7 @@ class WorkOrder(Document): bom.set_bom_material_details() return bom +@frappe.whitelist() def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): if txt: filters['operation'] = ('like', '%%%s%%' % txt) @@ -618,9 +640,10 @@ def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): filters = filters, fields = ['operation'], as_list=1) @frappe.whitelist() -def get_item_details(item, project = None): +def get_item_details(item, project = None, skip_bom_info=False): res = frappe.db.sql(""" - select stock_uom, description + 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) @@ -631,6 +654,7 @@ def get_item_details(item, project = None): return {} res = res[0] + if skip_bom_info: return res filters = {"item": item, "is_default": 1} @@ -662,7 +686,7 @@ def get_item_details(item, project = None): return res @frappe.whitelist() -def make_work_order(bom_no, item, qty=0, project=None): +def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): if not frappe.has_permission("Work Order", "write"): frappe.throw(_("Not permitted"), frappe.PermissionError) @@ -677,8 +701,44 @@ def make_work_order(bom_no, item, qty=0, project=None): wo_doc.qty = flt(qty) wo_doc.get_items_and_operations_from_bom() + if variant_items: + add_variant_item(variant_items, wo_doc, bom_no, "required_items") + return wo_doc +def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): + if isinstance(variant_items, string_types): + variant_items = json.loads(variant_items) + + for item in variant_items: + args = frappe._dict({ + "item_code": item.get("varint_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({ + "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("varint_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 } diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index 4442162636..3acf5727d1 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -1,526 +1,144 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-04-18 07:38:26.314642", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2016-04-18 07:38:26.314642", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "operation", + "item_code", + "source_warehouse", + "column_break_3", + "item_name", + "description", + "allow_alternative_item", + "include_item_in_manufacturing", + "qty_section", + "required_qty", + "rate", + "amount", + "column_break_11", + "transferred_qty", + "consumed_qty", + "available_qty_at_source_warehouse", + "available_qty_at_wip_warehouse" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "operation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Operation", - "length": 0, - "no_copy": 0, - "options": "Operation", - "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": "operation", + "fieldtype": "Link", + "label": "Operation", + "options": "Operation" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "source_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Source Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "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": "source_warehouse", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "Source Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "qty_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "qty_section", + "fieldtype": "Section Break", + "label": "Qty" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "required_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Required Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!parent.skip_transfer", - "fieldname": "transferred_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Transferred Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:!parent.skip_transfer", + "fieldname": "transferred_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Transferred Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "allow_alternative_item", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Allow Alternative Item", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "allow_alternative_item", + "fieldtype": "Check", + "label": "Allow Alternative Item" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "include_item_in_manufacturing", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Include Item In Manufacturing", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "include_item_in_manufacturing", + "fieldtype": "Check", + "label": "Include Item In Manufacturing" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_11", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!parent.skip_transfer", - "fieldname": "consumed_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Consumed Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:!parent.skip_transfer", + "fieldname": "consumed_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Consumed Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "available_qty_at_source_warehouse", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Available Qty at Source Warehouse", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "available_qty_at_source_warehouse", + "fieldtype": "Float", + "label": "Available Qty at Source Warehouse", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "available_qty_at_wip_warehouse", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Available Qty at WIP Warehouse", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "available_qty_at_wip_warehouse", + "fieldtype": "Float", + "label": "Available Qty at WIP Warehouse", + "read_only": 1 + }, + { + "fieldname": "rate", + "fieldtype": "Currency", + "label": "Rate", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-11-20 19:04:38.508839", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Work Order Item", - "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": "2020-04-13 18:46:32.966416", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Work Order Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json b/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json new file mode 100644 index 0000000000..7b5747e393 --- /dev/null +++ b/erpnext/manufacturing/module_onboarding/manufacturing/manufacturing.json @@ -0,0 +1,56 @@ +{ + "allow_roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Item Manager" + }, + { + "role": "Stock User" + } + ], + "creation": "2020-05-05 16:37:08.238935", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-08 14:05:56.197563", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing", + "owner": "Administrator", + "steps": [ + { + "step": "Warehouse" + }, + { + "step": "Workstation" + }, + { + "step": "Operation" + }, + { + "step": "Create Product" + }, + { + "step": "Create Raw Materials" + }, + { + "step": "Create BOM" + }, + { + "step": "Work Order" + }, + { + "step": "Explore Manufacturing Settings" + } + ], + "subtitle": "Products, Raw Materials, BOM, Work Order, and more.", + "success_message": "Manufacturing module is all set up!", + "title": "Let's Set Up the Manufacturing Module." +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding/manufacturing/manufacturing.json b/erpnext/manufacturing/onboarding/manufacturing/manufacturing.json new file mode 100644 index 0000000000..50584e19ca --- /dev/null +++ b/erpnext/manufacturing/onboarding/manufacturing/manufacturing.json @@ -0,0 +1,54 @@ +{ + "allow_roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Item Manager" + }, + { + "role": "Stock User" + } + ], + "creation": "2020-05-05 16:37:08.238935", + "docstatus": 0, + "doctype": "Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing", + "idx": 0, + "is_complete": 0, + "modified": "2020-05-12 16:22:07.050224", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing", + "owner": "Administrator", + "steps": [ + { + "step": "Introduction to Manufacturing" + }, + { + "step": "Warehouse" + }, + { + "step": "Workstation" + }, + { + "step": "Operation" + }, + { + "step": "Create Product" + }, + { + "step": "Create BOM" + }, + { + "step": "Work Order" + } + ], + "subtitle": "Products, Raw Materials, BOM, Work Order and more.", + "success_message": "Manufacturing module is all setup!", + "title": "Let's Setup Manufacturing Module", + "user_can_dismiss": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/create_bom/create_bom.json b/erpnext/manufacturing/onboarding_step/create_bom/create_bom.json new file mode 100644 index 0000000000..84b4088f23 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/create_bom/create_bom.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-05 16:41:20.239696", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:51:31.315686", + "modified_by": "Administrator", + "name": "Create BOM", + "owner": "Administrator", + "reference_document": "BOM", + "show_full_form": 1, + "title": "Create a BOM (Bill of Material)", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/create_product/create_product.json b/erpnext/manufacturing/onboarding_step/create_product/create_product.json new file mode 100644 index 0000000000..0ffa30158b --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/create_product/create_product.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-05 16:42:31.476275", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:50:59.010439", + "modified_by": "Administrator", + "name": "Create Product", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create a Finished Good", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/create_raw_materials/create_raw_materials.json b/erpnext/manufacturing/onboarding_step/create_raw_materials/create_raw_materials.json new file mode 100644 index 0000000000..0764f2e44a --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/create_raw_materials/create_raw_materials.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-19 11:53:17.295372", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 11:53:25.147837", + "modified_by": "Administrator", + "name": "Create Raw Materials", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create Raw Materials", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/explore_manufacturing_settings/explore_manufacturing_settings.json b/erpnext/manufacturing/onboarding_step/explore_manufacturing_settings/explore_manufacturing_settings.json new file mode 100644 index 0000000000..7ef202ee4e --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/explore_manufacturing_settings/explore_manufacturing_settings.json @@ -0,0 +1,20 @@ +{ + "action": "Show Form Tour", + "creation": "2020-05-19 11:55:11.378374", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 1, + "is_skipped": 0, + "modified": "2020-05-26 20:28:03.558199", + "modified_by": "Administrator", + "name": "Explore Manufacturing Settings", + "owner": "Administrator", + "reference_document": "Manufacturing Settings", + "show_full_form": 0, + "title": "Explore Manufacturing Settings", + "validate_action": 0, + "video_url": "https://www.youtube.com/watch?v=UVGfzwOOZC4" +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/introduction_to_manufacturing/introduction_to_manufacturing.json b/erpnext/manufacturing/onboarding_step/introduction_to_manufacturing/introduction_to_manufacturing.json new file mode 100644 index 0000000000..eb7ab3a175 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/introduction_to_manufacturing/introduction_to_manufacturing.json @@ -0,0 +1,20 @@ +{ + "action": "Update Settings", + "creation": "2020-05-05 16:40:23.676406", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 19:11:57.152883", + "modified_by": "Administrator", + "name": "Introduction to Manufacturing", + "owner": "Administrator", + "reference_document": "Manufacturing Settings", + "show_full_form": 0, + "title": "Manufacturing Settings", + "validate_action": 1, + "video_url": "https://www.youtube.com/watch?v=UVGfzwOOZC4" +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/operation/operation.json b/erpnext/manufacturing/onboarding_step/operation/operation.json new file mode 100644 index 0000000000..b532e6778c --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/operation/operation.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:15:31.706756", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:50:41.642754", + "modified_by": "Administrator", + "name": "Operation", + "owner": "Administrator", + "reference_document": "Operation", + "show_full_form": 0, + "title": "Create a Operation", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/warehouse/warehouse.json b/erpnext/manufacturing/onboarding_step/warehouse/warehouse.json new file mode 100644 index 0000000000..e23bd33b78 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/warehouse/warehouse.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:13:34.014554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:50:13.766712", + "modified_by": "Administrator", + "name": "Warehouse", + "owner": "Administrator", + "reference_document": "Warehouse", + "show_full_form": 0, + "title": "Create a Warehouse", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/work_order/work_order.json b/erpnext/manufacturing/onboarding_step/work_order/work_order.json new file mode 100644 index 0000000000..c63363e7cb --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/work_order/work_order.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:15:56.084682", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:51:38.133150", + "modified_by": "Administrator", + "name": "Work Order", + "owner": "Administrator", + "reference_document": "Work Order", + "show_full_form": 1, + "title": "Create a Work Order", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/onboarding_step/workstation/workstation.json b/erpnext/manufacturing/onboarding_step/workstation/workstation.json new file mode 100644 index 0000000000..df244bb494 --- /dev/null +++ b/erpnext/manufacturing/onboarding_step/workstation/workstation.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 16:14:14.930214", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:50:33.938176", + "modified_by": "Administrator", + "name": "Workstation", + "owner": "Administrator", + "reference_document": "Workstation", + "show_full_form": 0, + "title": "Create a Workstation / Machine", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_category/__init__.py b/erpnext/manufacturing/report/bom_operations_time/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_category/__init__.py rename to erpnext/manufacturing/report/bom_operations_time/__init__.py diff --git a/erpnext/accounts/report/ordered_items_to_be_billed/ordered_items_to_be_billed.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js similarity index 66% rename from erpnext/accounts/report/ordered_items_to_be_billed/ordered_items_to_be_billed.js rename to erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js index 6e13d67766..7468e34020 100644 --- a/erpnext/accounts/report/ordered_items_to_be_billed/ordered_items_to_be_billed.js +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js @@ -1,8 +1,9 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +/* eslint-disable */ -frappe.query_reports["Ordered Items To Be Billed"] = { +frappe.query_reports["BOM Operations Time"] = { "filters": [ ] -} +}; diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json new file mode 100644 index 0000000000..665c5b9f79 --- /dev/null +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json @@ -0,0 +1,28 @@ +{ + "add_total_row": 0, + "creation": "2020-03-03 01:41:20.862521", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "", + "modified": "2020-03-03 01:41:20.862521", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Operations Time", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "BOM", + "report_name": "BOM Operations Time", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing Manager" + }, + { + "role": "Manufacturing User" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py new file mode 100644 index 0000000000..e7d92658f7 --- /dev/null +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py @@ -0,0 +1,112 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + data = get_data(filters) + columns = get_columns(filters) + return columns, data + +def get_data(filters): + data = [] + + bom_data = [] + for d in frappe.db.sql(""" + SELECT + bom.name, bom.item, bom.item_name, bom.uom, + bomps.operation, bomps.workstation, bomps.time_in_mins + FROM `tabBOM` bom, `tabBOM Operation` bomps + WHERE + bom.docstatus = 1 and bom.is_active = 1 and bom.name = bomps.parent + """, as_dict=1): + row = get_args() + if d.name not in bom_data: + bom_data.append(d.name) + row.update(d) + else: + row.update({ + "operation": d.operation, + "workstation": d.workstation, + "time_in_mins": d.time_in_mins + }) + + data.append(row) + + used_as_subassembly_items = get_bom_count(bom_data) + + for d in data: + d.used_as_subassembly_items = used_as_subassembly_items.get(d.name, 0) + + return data + +def get_bom_count(bom_data): + data = frappe.get_all("BOM Item", + fields=["count(name) as count", "bom_no"], + filters= {"bom_no": ("in", bom_data)}, group_by = "bom_no") + + bom_count = {} + for d in data: + bom_count.setdefault(d.bom_no, d.count) + + return bom_count + +def get_args(): + return frappe._dict({ + "name": "", + "item": "", + "item_name": "", + "uom": "" + }) + +def get_columns(filters): + return [{ + "label": _("BOM ID"), + "options": "BOM", + "fieldname": "name", + "fieldtype": "Link", + "width": 140 + }, { + "label": _("BOM Item Code"), + "options": "Item", + "fieldname": "item", + "fieldtype": "Link", + "width": 140 + }, { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 110 + }, { + "label": _("UOM"), + "options": "UOM", + "fieldname": "uom", + "fieldtype": "Link", + "width": 140 + }, { + "label": _("Operation"), + "options": "Operation", + "fieldname": "operation", + "fieldtype": "Link", + "width": 120 + }, { + "label": _("Workstation"), + "options": "Workstation", + "fieldname": "workstation", + "fieldtype": "Link", + "width": 110 + }, { + "label": _("Time (In Mins)"), + "fieldname": "time_in_mins", + "fieldtype": "Int", + "width": 140 + }, { + "label": _("Sub-assembly BOM Count"), + "fieldname": "used_as_subassembly_items", + "fieldtype": "Int", + "width": 180 + }] + + 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 c5627e0c08..e3e440ebc6 100644 --- a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py +++ b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py @@ -19,7 +19,7 @@ def get_columns(filters): "options": "Work Order", "width": 120 }] - + if not filters.get('bom_no'): columns.extend([ { diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/__init__.py b/erpnext/manufacturing/report/downtime_analysis/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_declaration/__init__.py rename to erpnext/manufacturing/report/downtime_analysis/__init__.py diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js new file mode 100644 index 0000000000..ff32dbed98 --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js @@ -0,0 +1,28 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Downtime Analysis"] = { + "filters": [ + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Datetime", + default: frappe.datetime.add_months(frappe.datetime.now_datetime(), -1), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Datetime", + default: frappe.datetime.now_datetime(), + reqd: 1, + }, + { + label: __("Machine"), + fieldname: "workstation", + fieldtype: "Link", + options: "Workstation" + } + ] +}; diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.json b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.json new file mode 100644 index 0000000000..5edc7781a2 --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 1, + "creation": "2020-04-20 18:26:04.345289", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2020-04-20 18:26:04.345289", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Downtime Analysis", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Downtime Entry", + "report_name": "Downtime Analysis", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py new file mode 100644 index 0000000000..093309a005 --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py @@ -0,0 +1,113 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import flt +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + 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"] + + query_filters["from_time"] = (">=", filters.get("from_date")) + query_filters["to_time"] = ("<=", filters.get("to_date")) + + if filters.get("workstation"): + query_filters["workstation"] = filters.get("workstation") + + 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]))) + + workstation_wise_data = {} + for d in data: + if d.workstation not in workstation_wise_data: + workstation_wise_data[d.workstation] = 0 + + workstation_wise_data[d.workstation] += flt(d.downtime, 2) + + datasets = [] + for label in labels: + datasets.append(workstation_wise_data.get(label, 0)) + + chart = { + "data": { + "labels": labels, + "datasets": [ + {"name": "Machine Downtime", "values": datasets} + ] + }, + "type": "bar" + } + + return chart + +def get_columns(filters): + return [ + { + "label": _("ID"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Downtime Entry", + "width": 100 + }, + { + "label": _("Machine"), + "fieldname": "workstation", + "fieldtype": "Link", + "options": "Workstation", + "width": 100 + }, + { + "label": _("Operator"), + "fieldname": "operator", + "fieldtype": "Link", + "options": "Employee", + "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 + } + ] \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration_category/__init__.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_declaration_category/__init__.py rename to erpnext/manufacturing/report/exponential_smoothing_forecasting/__init__.py diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js new file mode 100644 index 0000000000..123a82a388 --- /dev/null +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js @@ -0,0 +1,97 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Exponential Smoothing Forecasting"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1 + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), 12), + "reqd": 1 + }, + { + "fieldname":"based_on_document", + "label": __("Based On Document"), + "fieldtype": "Select", + "options": ["Sales Order", "Delivery Note", "Quotation"], + "default": "Sales Order", + "reqd": 1 + }, + { + "fieldname":"based_on_field", + "label": __("Based On"), + "fieldtype": "Select", + "options": ["Qty", "Amount"], + "default": "Qty", + "reqd": 1 + }, + { + "fieldname":"no_of_years", + "label": __("Based On Data ( in years )"), + "fieldtype": "Select", + "options": [3, 6, 9], + "default": 3, + "reqd": 1 + }, + { + "fieldname": "periodicity", + "label": __("Periodicity"), + "fieldtype": "Select", + "options": [ + { "value": "Monthly", "label": __("Monthly") }, + { "value": "Quarterly", "label": __("Quarterly") }, + { "value": "Half-Yearly", "label": __("Half-Yearly") }, + { "value": "Yearly", "label": __("Yearly") } + ], + "default": "Yearly", + "reqd": 1 + }, + { + "fieldname":"smoothing_constant", + "label": __("Smoothing Constant"), + "fieldtype": "Select", + "options": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], + "reqd": 1, + "default": 0.3 + }, + { + "fieldname":"item_code", + "label": __("Item Code"), + "fieldtype": "Link", + "options": "Item" + }, + { + "fieldname":"warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + get_query: () => { + var company = frappe.query_report.get_filter_value('company'); + if (company) { + return { + filters: { + 'company': company + } + }; + } + } + } + ] +}; diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.json b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.json new file mode 100644 index 0000000000..5092ef4e7a --- /dev/null +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.json @@ -0,0 +1,40 @@ +{ + "add_total_row": 0, + "creation": "2020-05-15 05:18:55.838030", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "", + "modified": "2020-05-15 05:18:55.838030", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Exponential Smoothing Forecasting", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Exponential Smoothing Forecasting", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Stock Manager" + }, + { + "role": "Sales Manager" + }, + { + "role": "Sales User" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py new file mode 100644 index 0000000000..2ca9f1694b --- /dev/null +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -0,0 +1,242 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +from frappe import _ +from frappe.utils import flt, nowdate, add_years, cint, getdate +from erpnext.accounts.report.financial_statements import get_period_list +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(): + forecast_data = [] + for period in self.period_list: + forecast_key = "forecast_" + period.key + + if value.get(period.key) and not forecast_data: + value[forecast_key] = flt(value.get("avg", 0)) or flt(value.get(period.key)) + + 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]) + ) + ) + + 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.fieldtype = "Float" if self.based_on_field == "qty" else "Currency" + self.company_currency = erpnext.get_company_currency(self.filters.company) + + def execute_report(self): + self.prepare_periodical_data() + self.forecast_future_data() + self.prepare_final_data() + self.add_total() + + columns = self.get_columns() + charts = self.get_chart_data() + summary_data = self.get_summary_data() + + return columns, self.data, None, charts, summary_data + + def prepare_periodical_data(self): + 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, None, self.filters.periodicity, ignore_fiscal_year=True) + + order_data = self.get_data_for_forecast() or [] + + for entry in order_data: + key = (entry.item_code, entry.warehouse) + if key not in self.period_wise_data: + self.period_wise_data[key] = entry + + 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)) + + for key, value in self.period_wise_data.items(): + list_of_period_value = [value.get(p.key, 0) for p in self.period_list] + + if list_of_period_value: + total_qty = [1 for d in list_of_period_value if d] + if total_qty: + value["avg"] = flt(sum(list_of_period_value)) / flt(sum(total_qty)) + + 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)) + + warehouses = [] + if self.filters.warehouse: + warehouses = get_child_warehouses(self.filters.warehouse) + cond += " AND soi.warehouse in ({})".format(','.join(['%s'] * len(warehouses))) + + input_data = [self.filters.from_date, self.filters.company] + if warehouses: + input_data.extend(warehouses) + + date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date" + + 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 + FROM + `tab{doc}` so, `tab{child_doc}` soi + 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) + + def prepare_final_data(self): + self.data = [] + + 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 + + total_row = { + "item_code": _(frappe.bold("Total Quantity")) + } + + for value in self.data: + for period in self.period_list: + forecast_key = "forecast_" + period.key + if forecast_key not in total_row: + total_row.setdefault(forecast_key, 0.0) + + if period.key not in total_row: + total_row.setdefault(period.key, 0.0) + + total_row[forecast_key] += value.get(forecast_key, 0.0) + total_row[period.key] += value.get(period.key, 0.0) + + 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 + }] + + 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)): + + forecast_key = period.key + label = _(period.label) + if period.from_date >= getdate(self.filters.from_date): + forecast_key = 'forecast_' + period.key + label = _(period.label) + " " + _("(Forecast)") + + 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 + + labels = [] + self.total_demand = [] + self.total_forecast = [] + self.total_history_forecast = [] + self.total_future_forecast = [] + + for period in self.period_list: + forecast_key = "forecast_" + period.key + + labels.append(_(period.label)) + + if period.from_date < getdate(self.filters.from_date): + self.total_demand.append(self.data[-1].get(period.key, 0)) + self.total_history_forecast.append(self.data[-1].get(forecast_key, 0)) + else: + self.total_future_forecast.append(self.data[-1].get(forecast_key, 0)) + + self.total_forecast.append(self.data[-1].get(forecast_key, 0)) + + return { + "data": { + "labels": labels, + "datasets": [ + { + "name": "Demand", + "values": self.total_demand + }, + { + "name": "Forecast", + "values": self.total_forecast + } + ] + }, + "type": "line" + } + + def get_summary_data(self): + if not self.data: return + + return [ + { + "value": sum(self.total_demand), + "label": _("Total Demand (Past Data)"), + "currency": self.company_currency, + "datatype": self.fieldtype + }, + { + "value": sum(self.total_history_forecast), + "label": _("Total Forecast (Past Data)"), + "currency": self.company_currency, + "datatype": self.fieldtype + }, + { + "value": sum(self.total_future_forecast), + "indicator": "Green", + "label": _("Total Forecast (Future Data)"), + "currency": self.company_currency, + "datatype": self.fieldtype + } + ] \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/__init__.py b/erpnext/manufacturing/report/job_card_summary/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_proof_submission/__init__.py rename to erpnext/manufacturing/report/job_card_summary/__init__.py diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js new file mode 100644 index 0000000000..bd68db190e --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -0,0 +1,73 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Job Card Summary"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: frappe.defaults.get_user_default("fiscal_year"), + reqd: 1, + on_change: function(query_report) { + var fiscal_year = query_report.get_values().fiscal_year; + if (!fiscal_year) { + return; + } + frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); + frappe.query_report.set_filter_value({ + from_date: fy.year_start_date, + to_date: fy.year_end_date + }); + }); + } + }, + { + label: __("From Posting Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_start_date"), + reqd: 1 + }, + { + label: __("To Posting Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_end_date"), + reqd: 1, + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["", "Open", "Work In Progress", "Completed", "On Hold"] + }, + { + label: __("Sales Orders"), + fieldname: "sales_order", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Sales Order', txt); + } + }, + { + label: __("Production Item"), + fieldname: "production_item", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Item', txt); + } + } + ] +}; diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json new file mode 100644 index 0000000000..9f08fc34cb --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json @@ -0,0 +1,34 @@ +{ + "add_total_row": 0, + "creation": "2020-04-20 12:00:21.436619", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2020-04-20 12:00:21.436619", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Job Card", + "report_name": "Job Card Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + }, + { + "role": "Manufacturing Manager" + }, + { + "role": "Stock Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py new file mode 100644 index 0000000000..b1bff3500c --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py @@ -0,0 +1,204 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import getdate, flt +from erpnext.stock.report.stock_analytics.stock_analytics import (get_period_date_ranges, get_period) + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + 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]) + } + + fields = ["name", "status", "work_order", "production_item", "item_name", "posting_date", + "total_completed_qty", "workstation", "operation", "employee_name", "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) + + if not data: return [] + + job_cards = [d.name for d in data] + + job_card_time_filter = { + "docstatus": ("<", 2), + "parent": ("in", job_cards), + } + + job_card_time_details = {} + 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", debug=1): + job_card_time_details[job_card_data.parent] = job_card_data + + res = [] + for d in data: + if d.status != "Completed": + d.status = "Open" + + if job_card_time_details.get(d.name): + d.from_time = job_card_time_details.get(d.name).from_time + d.to_time = job_card_time_details.get(d.name).to_time + + res.append(d) + + return res + +def get_chart_data(job_card_details, filters): + labels, periodic_data = prepare_chart_data(job_card_details, filters) + + open_job_cards, completed = [], [] + datasets = [] + + for d in labels: + open_job_cards.append(periodic_data.get("Open").get(d)) + completed.append(periodic_data.get("Completed").get(d)) + + datasets.append({"name": "Open", "values": open_job_cards}) + datasets.append({"name": "Completed", "values": completed}) + + chart = { + "data": { + 'labels': labels, + 'datasets': datasets + }, + "type": "bar" + } + + return chart + +def prepare_chart_data(job_card_details, filters): + labels = [] + + periodic_data = { + "Open": {}, + "Completed": {} + } + + filters.range = "Monthly" + + ranges = get_period_date_ranges(filters) + for from_date, end_date in ranges: + period = get_period(end_date, filters) + if period not in labels: + labels.append(period) + + for d in job_card_details: + if getdate(d.posting_date) > from_date and getdate(d.posting_date) <= end_date: + status = "Completed" if d.status == "Completed" else "Open" + + if periodic_data.get(status).get(period): + periodic_data[status][period] += 1 + else: + periodic_data[status][period] = 1 + + return labels, periodic_data + +def get_columns(filters): + columns = [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Job Card", + "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 + }, + ) + + 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": _("Employee Name"), + "fieldname": "employee_name", + "fieldtype": "Data", + "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 \ No newline at end of file diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index 7447a1f670..79af8a1e39 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -55,32 +55,27 @@ def get_periodic_data(filters, entry): 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) : periodic_data = update_periodic_data(periodic_data, "Pending", period) - 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) : periodic_data = update_periodic_data(periodic_data, "Pending", period) - 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) : 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): @@ -148,4 +143,3 @@ def get_chart_data(periodic_data, columns): - diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission_detail/__init__.py b/erpnext/manufacturing/report/production_planning_report/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_proof_submission_detail/__init__.py rename to erpnext/manufacturing/report/production_planning_report/__init__.py diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.js b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js new file mode 100644 index 0000000000..675b8a1100 --- /dev/null +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.js @@ -0,0 +1,111 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Production Planning Report"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname":"based_on", + "label": __("Based On"), + "fieldtype": "Select", + "options": ["Sales Order", "Material Request", "Work Order"], + "default": "Sales Order", + "reqd": 1, + on_change: function() { + let filters = frappe.query_report.filters; + let based_on = frappe.query_report.get_filter_value('based_on'); + let options = { + "Sales Order": ["Delivery Date", "Total Amount"], + "Material Request": ["Required Date"], + "Work Order": ["Planned Start Date"] + } + + filters.forEach(d => { + if (d.fieldname == "order_by") { + d.df.options = options[based_on]; + d.set_input(d.df.options) + } + }); + + frappe.query_report.refresh(); + } + }, + { + "fieldname":"docnames", + "label": __("Document Name"), + "fieldtype": "MultiSelectList", + "options": "Sales Order", + "get_data": function(txt) { + if (!frappe.query_report.filters) return; + + let based_on = frappe.query_report.get_filter_value('based_on'); + if (!based_on) return; + + return frappe.db.get_link_options(based_on, txt); + }, + "get_query": function() { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + "docstatus": 1, + "company": company + } + }; + } + }, + { + "fieldname":"raw_material_warehouse", + "label": __("Raw Material Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "depends_on": "eval: doc.based_on != 'Work Order'", + "get_query": function() { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + "company": company + } + }; + } + }, + { + "fieldname":"order_by", + "label": __("Order By"), + "fieldtype": "Select", + "options": ["Delivery Date", "Total Amount"], + "default": "Delivery Date" + }, + { + "fieldname":"include_subassembly_raw_materials", + "label": __("Include Sub-assembly Raw Materials"), + "fieldtype": "Check", + "depends_on": "eval: doc.based_on != 'Work Order'", + "default": 0 + }, + ], + "formatter": function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname == "production_item_name" && data && data.qty_to_manufacture > data.available_qty ) { + value = `
${value}
`; + } + + if (column.fieldname == "production_item" && !data.name ) { + value = ""; + } + + if (column.fieldname == "raw_material_name" && data && data.required_qty > data.allotted_qty ) { + value = `
${value}
`; + } + + return value; + }, +}; diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.json b/erpnext/manufacturing/report/production_planning_report/production_planning_report.json new file mode 100644 index 0000000000..f37dad39a4 --- /dev/null +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 0, + "creation": "2020-03-06 11:37:43.180095", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "", + "modified": "2020-03-06 11:38:05.789851", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Planning Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Production Planning Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + }, + { + "role": "Manufacturing Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py new file mode 100644 index 0000000000..5ac3923187 --- /dev/null +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -0,0 +1,374 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses + +# and bom_no is not null and bom_no !='' + +mapper = { + "Sales Order": { + "fields": """ item_code as production_item, item_name as production_item_name, stock_uom, + 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""" + }, + "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' """ + }, + "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')" + }, +} + +order_mapper = { + "Sales Order": { + "Delivery Date": "`tabSales Order Item`.delivery_date asc", + "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" + } +} + +def execute(filters=None): + return ProductionPlanReport(filters).execute_report() + +class ProductionPlanReport(object): + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + self.raw_materials_dict = {} + self.data = [] + + def execute_report(self): + self.get_open_orders() + self.get_raw_materials() + self.get_item_details() + self.get_bin_details() + self.get_purchase_details() + self.prepare_data() + self.get_columns() + + 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)) + + 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} + 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) + + 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))) + + 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)) + + return filters + + def get_raw_materials(self): + 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 [] + self.warehouses.extend([d.source_warehouse for d in raw_materials]) + + else: + bom_nos = [] + + for d in self.orders: + bom_no = d.bom_no or frappe.get_cached_value("Item", d.production_item, "default_bom") + + if not d.bom_no: + d.bom_no = bom_no + + bom_nos.append(bom_no) + + 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)") + + 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 + 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) + + if not raw_materials: return + + self.item_codes.extend([d.item_code for d in raw_materials]) + + for d in raw_materials: + if d.parent not in self.raw_materials_dict: + self.raw_materials_dict.setdefault(d.parent, []) + + rows = self.raw_materials_dict[d.parent] + rows.append(d) + + def get_item_details(self): + 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)}): + self.item_details[d.parent] = d + + def get_bin_details(self): + if not (self.orders and self.raw_materials_dict): return + + self.bin_details = {} + self.mrp_warehouses = [] + if self.filters.raw_material_warehouse: + 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", + fields=["warehouse", "item_code", "actual_qty", "ordered_qty", "projected_qty"], + 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 + + self.purchase_details = {} + + for d in 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)}, + group_by = "item_code, warehouse"): + 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 + + 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 + + bin_data = self.bin_details.get((d.production_item, d.warehouse)) or {} + 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) + + bin_data["actual_qty"] -= d.available_qty + + self.update_raw_materials(d, key) + + def update_raw_materials(self, data, key): + self.index = 0 + self.raw_materials_dict.get(key) + + warehouses = self.mrp_warehouses or [] + for d in self.raw_materials_dict.get(key): + if self.filters.based_on != "Work Order": + d.required_qty = d.required_qty * data.qty_to_manufacture + + if not warehouses: + warehouses = [data.warehouse] + + if self.filters.based_on == "Work Order" and d.warehouse: + warehouses = [d.warehouse] + else: + item_details = self.item_details.get(d.item_code) + if item_details: + warehouses = [item_details["default_warehouse"]] + + if self.filters.raw_material_warehouse: + warehouses = get_child_warehouses(self.filters.raw_material_warehouse) + + 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): + row = self.get_args() + d.warehouse = self.filters.raw_material_warehouse + d.required_qty = d.remaining_qty + d.allotted_qty = 0 + row.update(d) + self.data.append(row) + + def pick_materials_from_warehouses(self, args, order_data, warehouses): + for index, warehouse in enumerate(warehouses): + if not args.remaining_qty: return + + row = self.get_args() + + key = (args.item_code, warehouse) + bin_data = self.bin_details.get(key) + + if bin_data: + row.update(bin_data) + + 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.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 not self.index: + row.update(order_data) + self.index += 1 + + args.warehouse = warehouse + row.update(args) + if self.purchase_details.get(key): + row.update(self.purchase_details.get(key)) + + 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": "" + }) + + 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 + }] + + fieldname, fieldtype = "delivery_date", "Date" + if self.filters.based_on == "Sales Order" and self.filters.order_by == "Total Amount": + fieldname, fieldtype = "base_grand_total", "Currency" + elif self.filters.based_on == "Material Request": + fieldname = "schedule_date" + 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.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 + }]) + +def document_query(doctype, txt, searchfield, start, page_len, filters): + pass \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_sub_category/__init__.py b/erpnext/manufacturing/report/quality_inspection_summary/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_sub_category/__init__.py rename to erpnext/manufacturing/report/quality_inspection_summary/__init__.py diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js new file mode 100644 index 0000000000..d4587aa661 --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.js @@ -0,0 +1,40 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Quality Inspection Summary"] = { + "filters": [ + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -12), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["", "Accepted", "Rejected"] + }, + { + label: __("Item Code"), + fieldname: "item_code", + fieldtype: "Link", + options: "Item" + }, + { + label: __("Inspected By"), + fieldname: "inspected_by", + fieldtype: "Link", + options: "User" + } + ] +}; diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.json b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.json new file mode 100644 index 0000000000..48226e6b21 --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "creation": "2020-04-26 18:23:53.475110", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "letter_head": "Gadgets International", + "modified": "2020-04-26 18:24:50.529940", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Quality Inspection Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Quality Inspection", + "report_name": "Quality Inspection Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Quality Manager" + }, + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py new file mode 100644 index 0000000000..6192632bda --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -0,0 +1,132 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_columns(filters) + chart_data = get_chart_data(data, filters) + 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"] + + for field in ["status", "item_code", "status", "inspected_by"]: + if filters.get(field): + query_filters[field] = ("in", filters.get(field)) + + 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") + +def get_chart_data(periodic_data, columns): + labels = ["Rejected", "Accepted"] + + 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")]}) + + chart = { + "data": { + 'labels': labels, + 'datasets': datasets + }, + "type": "donut", + "height": 300 + } + + return chart + +def get_columns(filters): + columns = [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": 100 + }, + { + "label": _("Report Date"), + "fieldname": "report_date", + "fieldtype": "Date", + "width": 150 + } + ] + + if not filters.get("status"): + columns.append( + { + "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 + } + ]) + + return columns \ No newline at end of file diff --git a/erpnext/hr/doctype/income_tax_slab/__init__.py b/erpnext/manufacturing/report/work_order_summary/__init__.py similarity index 100% rename from erpnext/hr/doctype/income_tax_slab/__init__.py rename to erpnext/manufacturing/report/work_order_summary/__init__.py diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.js b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js new file mode 100644 index 0000000000..eb23f17c47 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.js @@ -0,0 +1,86 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Work Order Summary"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: frappe.defaults.get_user_default("fiscal_year"), + reqd: 1, + on_change: function(query_report) { + var fiscal_year = query_report.get_values().fiscal_year; + if (!fiscal_year) { + return; + } + frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); + frappe.query_report.set_filter_value({ + from_date: fy.year_start_date, + to_date: fy.year_end_date + }); + }); + } + }, + { + label: __("From Posting Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_start_date"), + reqd: 1 + }, + { + label: __("To Posting Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.defaults.get_user_default("year_end_date"), + reqd: 1, + }, + { + label: __("Status"), + fieldname: "status", + fieldtype: "Select", + options: ["", "Not Started", "In Process", "Completed", "Stopped"] + }, + { + label: __("Sales Orders"), + fieldname: "sales_order", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Sales Order', txt); + } + }, + { + label: __("Production Item"), + fieldname: "production_item", + fieldtype: "MultiSelectList", + get_data: function(txt) { + return frappe.db.get_link_options('Item', txt); + } + }, + { + label: __("Age"), + fieldname:"age", + fieldtype: "Int", + default: "0" + }, + { + label: __("Charts Based On"), + fieldname:"charts_based_on", + fieldtype: "Select", + options: ["Status", "Age", "Quantity"], + default: "Status" + }, + ] +}; diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.json b/erpnext/manufacturing/report/work_order_summary/work_order_summary.json new file mode 100644 index 0000000000..0d093e22e9 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 0, + "creation": "2020-04-17 17:07:56.830358", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Gadgets International", + "modified": "2020-04-19 16:59:47.979278", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Work Order Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Work Order Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + }, + { + "role": "Manufacturing Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py new file mode 100644 index 0000000000..fb047b230c --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -0,0 +1,267 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import date_diff, today, getdate, flt +from frappe import _ +from erpnext.stock.report.stock_analytics.stock_analytics import (get_period_date_ranges, get_period) + +def execute(filters=None): + columns, data = [], [] + + if not filters.get("age"): + filters["age"] = 0 + + data = get_data(filters) + columns = get_columns(filters) + chart_data = get_chart_data(data, filters) + return columns, data, None, chart_data + +def get_data(filters): + query_filters = {"docstatus": 1} + + 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): + query_filters[field] = ("in", filters.get(field)) + + 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") + + res = [] + for d in data: + start_date = d.actual_start_date or d.planned_start_date + d.age = 0 + + if d.status != 'Completed': + d.age = date_diff(today(), start_date) + + if filters.get("age") <= d.age: + res.append(d) + + return res + +def get_chart_data(data, filters): + if filters.get("charts_based_on") == "Status": + return get_chart_based_on_status(data) + elif filters.get("charts_based_on") == "Age": + return get_chart_based_on_age(data) + else: + return get_chart_based_on_qty(data, filters) + +def get_chart_based_on_status(data): + labels = ["Completed", "In Process", "Stopped", "Not Started"] + + status_wise_data = { + "Not Started": 0, + "In Process": 0, + "Stopped": 0, + "Completed": 0 + } + + for d in data: + status_wise_data[d.status] += 1 + + values = [status_wise_data["Completed"], status_wise_data["In Process"], + status_wise_data["Stopped"], status_wise_data["Not Started"]] + + chart = { + "data": { + 'labels': labels, + 'datasets': [{'name':'Qty Wise Chart', 'values': values}] + }, + "type": "donut", + "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 + } + + for d in data: + if d.age > 0 and d.age <= 30: + age_wise_data["0-30 Days"] += 1 + elif d.age > 30 and d.age <= 60: + age_wise_data["30-60 Days"] += 1 + elif d.age > 60 and d.age <= 90: + age_wise_data["60-90 Days"] += 1 + 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"]] + + chart = { + "data": { + 'labels': labels, + 'datasets': [{'name':'Qty Wise Chart', 'values': values}] + }, + "type": "donut", + "height": 300 + } + + return chart + +def get_chart_based_on_qty(data, filters): + labels, periodic_data = prepare_chart_data(data, filters) + + pending, completed = [], [] + datasets = [] + + for d in labels: + pending.append(periodic_data.get("Pending").get(d)) + completed.append(periodic_data.get("Completed").get(d)) + + datasets.append({"name": "Pending", "values": pending}) + datasets.append({"name": "Completed", "values": completed}) + + chart = { + "data": { + 'labels': labels, + 'datasets': datasets + }, + "type": "bar", + "barOptions": { + "stacked": 1 + } + } + + return chart + +def prepare_chart_data(data, filters): + labels = [] + + periodic_data = { + "Pending": {}, + "Completed": {} + } + + filters.range = "Monthly" + + ranges = get_period_date_ranges(filters) + for from_date, end_date in ranges: + period = get_period(end_date, filters) + if period not in labels: + labels.append(period) + + if period not in periodic_data["Pending"]: + periodic_data["Pending"][period] = 0 + + if period not in periodic_data["Completed"]: + periodic_data["Completed"][period] = 0 + + 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["Completed"][period] += flt(d.produced_qty) + + return labels, periodic_data + +def get_columns(filters): + columns = [ + { + "label": _("Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": 100 + }, + ] + + if not filters.get("status"): + columns.append( + { + "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([ + { + "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 \ No newline at end of file diff --git a/erpnext/modules.txt b/erpnext/modules.txt index 3b347582c3..1e2aeea36a 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -24,4 +24,5 @@ Hotels Hub Node Quality Management Communication -Loan Management \ No newline at end of file +Loan Management +Payroll \ No newline at end of file diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 571f87af87..d1294ccc08 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -77,6 +77,7 @@ def create_customer(user_details): customer = frappe.new_doc("Customer") customer.customer_name = user_details.fullname customer.customer_type = "Individual" + customer.flags.ignore_mandatory = True customer.insert(ignore_permissions=True) try: @@ -91,7 +92,11 @@ def create_customer(user_details): "link_name": customer.name }) - contact.insert() + contact.save() + + except frappe.DuplicateEntryError: + return customer.name + except Exception as e: frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed")) pass diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 5a69cdb6ab..7a0caed621 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -62,10 +62,27 @@ def get_member_based_on_subscription(subscription_id, email): 'subscription_id': subscription_id, 'email_id': email }, order_by="creation desc") - return frappe.get_doc("Member", members[0]['name']) + try: + return frappe.get_doc("Member", members[0]['name']) + except: + return None + +def verify_signature(data): + signature = frappe.request.headers.get('X-Razorpay-Signature') + + settings = frappe.get_doc("Membership Settings") + key = settings.get_webhook_secret() + + controller = frappe.get_doc("Razorpay Settings") + + controller.verify_signature(data, signature, key) + @frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(data): +def trigger_razorpay_subscription(*args, **kwargs): + data = frappe.request.get_data(as_text=True) + verify_signature(data) + if isinstance(data, six.string_types): data = json.loads(data) data = frappe._dict(data) @@ -82,7 +99,10 @@ def trigger_razorpay_subscription(data): except Exception as e: error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) notify_failure(error_log) - raise e + return False + + if not member: + return False if data.event == "subscription.activated": member.customer_id = payment.customer_id @@ -111,7 +131,6 @@ def trigger_razorpay_subscription(data): return True - def notify_failure(log): try: content = """Dear System Manager, diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index c01a0b23d5..8c0e3a4fa7 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -1,8 +1,30 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Membership Settings', { +frappe.ui.form.on("Membership Settings", { refresh: function(frm) { + if (frm.doc.webhook_secret) { + frm.add_custom_button(__("Revoke "), () => { + frm.call("revoke_key").then(() => { + frm.refresh(); + }) + }); + } + frm.trigger("add_generate_button"); + }, - } + add_generate_button: function(frm) { + let label; + + if (frm.doc.webhook_secret) { + label = __("Regenerate Webhook Secret"); + } else { + label = __("Generate Webhook Secret"); + } + frm.add_custom_button(label, () => { + frm.call("generate_webhook_key").then(() => { + frm.refresh(); + }); + }); + }, }); diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 56b8eac4b1..52b9d01088 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -8,7 +8,8 @@ "enable_razorpay", "razorpay_settings_section", "billing_cycle", - "billing_frequency" + "billing_frequency", + "webhook_secret" ], "fields": [ { @@ -34,11 +35,17 @@ "fieldname": "billing_frequency", "fieldtype": "Int", "label": "Billing Frequency" + }, + { + "fieldname": "webhook_secret", + "fieldtype": "Password", + "label": "Webhook Secret", + "read_only": 1 } ], "issingle": 1, "links": [], - "modified": "2020-04-07 18:42:51.496807", + "modified": "2020-05-22 12:38:27.103759", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.py b/erpnext/non_profit/doctype/membership_settings/membership_settings.py index 2b8e37f2a6..f3b2eee6f9 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.py +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.py @@ -4,11 +4,27 @@ from __future__ import unicode_literals import frappe +from frappe import _ from frappe.integrations.utils import get_payment_gateway_controller from frappe.model.document import Document class MembershipSettings(Document): - pass + def generate_webhook_key(self): + key = frappe.generate_hash(length=20) + self.webhook_secret = key + self.save() + + frappe.msgprint( + _("Here is your webhook secret, this will be shown to you only once.") + "

" + key, + _("Webhook Secret") + ); + + def revoke_key(self): + self.webhook_secret = None; + self.save() + + def get_webhook_secret(self): + return self.get_password(fieldname="webhook_secret", raise_exception=False) @frappe.whitelist() def get_plans_for_membership(*args, **kwargs): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 38ed919c35..d9d3a4aadf 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -333,7 +333,7 @@ erpnext.patches.v7_0.update_mode_of_payment_type execute:frappe.reload_doctype('Employee') #2016-10-18 execute:frappe.db.sql("update `tabEmployee` set prefered_contact_email = IFNULL(prefered_contact_email,'') ") -execute:frappe.reload_doctype("Salary Slip") +execute:frappe.reload_doc("Payroll", "doctype", "salary_slip") execute:frappe.db.sql("update `tabSalary Slip` set posting_date=creation") execute:frappe.reload_doc("stock", "doctype", "stock_settings") erpnext.patches.v8_0.create_domain_docs #16-05-2017 @@ -678,11 +678,35 @@ erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.retain_permission_rules_for_video_doctype -erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries +erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22 erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive +execute:frappe.reload_doc("HR", "doctype", "Employee Advance") +erpnext.patches.v12_0.move_due_advance_amount_to_pending_amount execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True) erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price erpnext.patches.v12_0.set_valid_till_date_in_supplier_quotation erpnext.patches.v13_0.update_old_loans -erpnext.patches.v12_0.set_serial_no_status +erpnext.patches.v12_0.set_serial_no_status #2020-05-21 +erpnext.patches.v12_0.update_price_list_currency_in_bom +execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts') +erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo +erpnext.patches.v13_0.set_company_field_in_healthcare_doctypes #2020-05-25 +erpnext.patches.v12_0.update_bom_in_so_mr +execute:frappe.delete_doc("Report", "Department Analytics") +execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True) +erpnext.patches.v12_0.update_uom_conversion_factor +erpnext.patches.v13_0.delete_old_purchase_reports +erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions +erpnext.patches.v12_0.unhide_cost_center_field +erpnext.patches.v13_0.update_sla_enhancements +erpnext.patches.v12_0.update_address_template_for_india +erpnext.patches.v13_0.update_deferred_settings +erpnext.patches.v12_0.set_multi_uom_in_rfq +erpnext.patches.v13_0.delete_old_sales_reports +execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation") +erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020 +erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020 +erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020 +erpnext.patches.v12_0.add_taxjar_integration_field +erpnext.patches.v12_0.update_item_tax_template_company 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 f09c5b2c5d..e9b5950a13 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 @@ -6,8 +6,9 @@ from frappe.utils.nestedset import rebuild_tree def execute(): frappe.local.lang = frappe.db.get_default("lang") or 'en' - for doctype in ['department', 'leave_period', 'staffing_plan', 'job_opening', 'payroll_entry']: + for doctype in ['department', 'leave_period', 'staffing_plan', 'job_opening']: frappe.reload_doc("hr", "doctype", doctype) + frappe.reload_doc("Payroll", "doctype", 'payroll_entry') companies = frappe.db.get_all("Company", fields=["name", "abbr"]) departments = frappe.db.get_all("Department") diff --git a/erpnext/patches/v11_0/create_salary_structure_assignments.py b/erpnext/patches/v11_0/create_salary_structure_assignments.py index 610fa85172..c51c38182c 100644 --- a/erpnext/patches/v11_0/create_salary_structure_assignments.py +++ b/erpnext/patches/v11_0/create_salary_structure_assignments.py @@ -5,11 +5,11 @@ from __future__ import unicode_literals import frappe from datetime import datetime from frappe.utils import getdate -from erpnext.hr.doctype.salary_structure_assignment.salary_structure_assignment import DuplicateAssignment +from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import DuplicateAssignment def execute(): - frappe.reload_doc('hr', 'doctype', 'salary_structure') - frappe.reload_doc("hr", "doctype", "salary_structure_assignment") + frappe.reload_doc('Payroll', 'doctype', 'salary_structure') + frappe.reload_doc("Payroll", "doctype", "salary_structure_assignment") frappe.db.sql(""" delete from `tabSalary Structure Assignment` where salary_structure in (select name from `tabSalary Structure` where is_active='No' or docstatus!=1) 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 48249c9bbf..730eebc01c 100644 --- a/erpnext/patches/v11_0/inter_state_field_for_gst.py +++ b/erpnext/patches/v11_0/inter_state_field_for_gst.py @@ -6,8 +6,8 @@ def execute(): company = frappe.get_all('Company', filters = {'country': 'India'}) if not company: return - frappe.reload_doc("hr", "doctype", "Employee Tax Exemption Declaration") - frappe.reload_doc("hr", "doctype", "Employee Tax Exemption Proof Submission") + frappe.reload_doc("Payroll", "doctype", "Employee Tax Exemption Declaration") + frappe.reload_doc("Payroll", "doctype", "Employee Tax Exemption Proof Submission") frappe.reload_doc("hr", "doctype", "Employee Grade") frappe.reload_doc("hr", "doctype", "Leave Policy") diff --git a/erpnext/patches/v11_0/set_salary_component_properties.py b/erpnext/patches/v11_0/set_salary_component_properties.py index 83fb53d2a7..2498888273 100644 --- a/erpnext/patches/v11_0/set_salary_component_properties.py +++ b/erpnext/patches/v11_0/set_salary_component_properties.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doc('hr', 'doctype', 'salary_detail') - frappe.reload_doc('hr', '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'") diff --git a/erpnext/patches/v11_1/rename_depends_on_lwp.py b/erpnext/patches/v11_1/rename_depends_on_lwp.py index 20d8867e1d..a0f2536f7d 100644 --- a/erpnext/patches/v11_1/rename_depends_on_lwp.py +++ b/erpnext/patches/v11_1/rename_depends_on_lwp.py @@ -9,5 +9,5 @@ from frappe.model.utils.rename_field import rename_field def execute(): for doctype in ("Salary Component", "Salary Detail"): if "depends_on_lwp" in frappe.db.get_table_columns(doctype): - frappe.reload_doc("hr", "doctype", scrub(doctype)) + frappe.reload_doc("Payroll", "doctype", scrub(doctype)) rename_field(doctype, "depends_on_lwp", "depends_on_payment_days") \ No newline at end of file diff --git a/erpnext/patches/v12_0/add_taxjar_integration_field.py b/erpnext/patches/v12_0/add_taxjar_integration_field.py new file mode 100644 index 0000000000..4c823e13bd --- /dev/null +++ b/erpnext/patches/v12_0/add_taxjar_integration_field.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals + +import frappe +from erpnext.regional.united_states.setup import make_custom_fields + + +def execute(): + company = frappe.get_all('Company', filters={'country': 'United States'}) + if not company: + return + + make_custom_fields() 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 82c8f5c414..43bd0ccdd7 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 @@ -5,6 +5,8 @@ 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) company = frappe.get_all('Company', filters = {'country': 'United States'}) if not company: 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 3c9758eb84..1ddbae6cd2 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 @@ -4,7 +4,7 @@ import frappe def execute(): frappe.reload_doc('accounts', 'doctype', 'bank', force=1) - if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account'): + 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, b.branch_code = ba.branch_code @@ -12,4 +12,4 @@ def execute(): """) frappe.reload_doc('accounts', 'doctype', 'bank_account') - frappe.reload_doc('accounts', 'doctype', 'payment_request') \ No newline at end of file + frappe.reload_doc('accounts', 'doctype', 'payment_request') 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 new file mode 100644 index 0000000000..6013eaa29c --- /dev/null +++ b/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py @@ -0,0 +1,11 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + ''' 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 ''') 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 98a2fcf27e..24286dcebf 100644 --- a/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py @@ -6,6 +6,7 @@ import frappe def execute(): """Delete duplicate leave ledger entries of type allocation created.""" + frappe.reload_doc('hr', 'doctype', 'leave_ledger_entry') if not frappe.db.a_row_exists("Leave Ledger Entry"): return @@ -14,31 +15,32 @@ def execute(): def get_duplicate_records(): """Fetch all but one duplicate records from the list of expired leave allocation.""" - return frappe.db.sql_list(""" - WITH duplicate_records AS - (SELECT - name, transaction_name, is_carry_forward, - ROW_NUMBER() over(partition by transaction_name order by creation)as row - FROM `tabLeave Ledger Entry` l - WHERE (EXISTS - (SELECT name - FROM `tabLeave Ledger Entry` - WHERE - transaction_name = l.transaction_name - AND transaction_type = 'Leave Allocation' - AND name <> l.name - AND employee = l.employee - AND docstatus = 1 - AND leave_type = l.leave_type - AND is_carry_forward=l.is_carry_forward - AND to_date = l.to_date - AND from_date = l.from_date - AND is_expired = 1 - ))) - SELECT name FROM duplicate_records WHERE row > 1 + return frappe.db.sql(""" + SELECT name, employee, transaction_name, leave_type, is_carry_forward, from_date, to_date + FROM `tabLeave Ledger Entry` + WHERE + transaction_type = 'Leave Allocation' + AND docstatus = 1 + AND is_expired = 1 + GROUP BY + employee, transaction_name, leave_type, is_carry_forward, from_date, to_date + HAVING + count(name) > 1 + ORDER BY + creation """) def delete_duplicate_ledger_entries(duplicate_records_list): """Delete duplicate leave ledger entries.""" - if duplicate_records_list: - frappe.db.sql(''' DELETE FROM `tabLeave Ledger Entry` WHERE name in {0}'''.format(tuple(duplicate_records_list))) #nosec \ No newline at end of file + if not duplicate_records_list: return + for d in duplicate_records_list: + frappe.db.sql(''' + DELETE FROM `tabLeave Ledger Entry` + WHERE name != %s + AND employee = %s + AND transaction_name = %s + AND leave_type = %s + AND is_carry_forward = %s + AND from_date = %s + AND to_date = %s + ''', tuple(d)) \ No newline at end of file diff --git a/erpnext/patches/v12_0/rename_bank_reconciliation.py b/erpnext/patches/v12_0/rename_bank_reconciliation.py index eda47a95e0..2efa854fba 100644 --- a/erpnext/patches/v12_0/rename_bank_reconciliation.py +++ b/erpnext/patches/v12_0/rename_bank_reconciliation.py @@ -8,9 +8,6 @@ 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 Detail', 'Bank Clearance Detail', force=True) frappe.reload_doc('Accounts', 'doctype', 'Bank Clearance Detail') - - frappe.delete_doc("DocType", "Bank Reconciliation") - frappe.delete_doc("DocType", "Bank Reconciliation Detail") diff --git a/erpnext/patches/v12_0/set_italian_import_supplier_invoice_permissions.py b/erpnext/patches/v12_0/set_italian_import_supplier_invoice_permissions.py new file mode 100644 index 0000000000..a6011c4dac --- /dev/null +++ b/erpnext/patches/v12_0/set_italian_import_supplier_invoice_permissions.py @@ -0,0 +1,12 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from erpnext.regional.italy.setup import add_permissions + +def execute(): + countries = frappe.get_all("Company", fields="country") + countries = [country["country"] for country in countries] + if "Italy" in countries: + add_permissions() \ No newline at end of file diff --git a/erpnext/patches/v12_0/set_multi_uom_in_rfq.py b/erpnext/patches/v12_0/set_multi_uom_in_rfq.py new file mode 100644 index 0000000000..70ca6b222e --- /dev/null +++ b/erpnext/patches/v12_0/set_multi_uom_in_rfq.py @@ -0,0 +1,16 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import flt +from erpnext.stock.get_item_details import get_conversion_factor + +def execute(): + frappe.reload_doc('buying', 'doctype', 'request_for_quotation_item') + + frappe.db.sql("""UPDATE `tabRequest for Quotation Item` + SET + stock_uom = uom, + conversion_factor = 1, + stock_qty = qty""") \ No newline at end of file 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 6f843cdabd..52c9a2d7b3 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 @@ -64,11 +64,13 @@ def execute(): return_document_map = make_return_document_map(doctype, return_document_map) + count = 0 + #iterate through original documents and its return documents for docname in return_document_map: - doc_items = frappe.get_doc(doctype, docname).get("items") + doc_items = frappe.get_cached_doc(doctype, docname).get("items") for return_doc in return_document_map[docname]: - return_doc_items = frappe.get_doc(doctype, return_doc).get("items") + return_doc_items = frappe.get_cached_doc(doctype, return_doc).get("items") #iterate through return document items and original document items for mapping for return_item in return_doc_items: @@ -80,9 +82,11 @@ def execute(): else: continue + # commit after every 100 sql updates + count += 1 + if count%100 == 0: + frappe.db.commit() + set_document_detail_in_return_document("Purchase Receipt") set_document_detail_in_return_document("Delivery Note") frappe.db.commit() - - - diff --git a/erpnext/patches/v12_0/set_serial_no_status.py b/erpnext/patches/v12_0/set_serial_no_status.py index 4ec84ef0f9..3b5f5ef340 100644 --- a/erpnext/patches/v12_0/set_serial_no_status.py +++ b/erpnext/patches/v12_0/set_serial_no_status.py @@ -5,13 +5,22 @@ from frappe.utils import getdate, nowdate def execute(): frappe.reload_doc('stock', 'doctype', 'serial_no') - for serial_no in frappe.db.sql("""select name, delivery_document_type, warranty_expiry_date 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()): status = "Expired" + elif not serial_no.get("warehouse"): + status = "Inactive" else: status = "Active" - frappe.db.set_value("Serial No", serial_no.get("name"), "status", status) \ No newline at end of file + frappe.db.set_value("Serial No", serial_no.get("name"), "status", status) + + if frappe.db.auto_commit_on_many_writes: + frappe.db.auto_commit_on_many_writes = False diff --git a/erpnext/patches/v12_0/unhide_cost_center_field.py b/erpnext/patches/v12_0/unhide_cost_center_field.py new file mode 100644 index 0000000000..6005ab7072 --- /dev/null +++ b/erpnext/patches/v12_0/unhide_cost_center_field.py @@ -0,0 +1,13 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + 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' + """) \ No newline at end of file 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 60aec05466..b8efb210a0 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,15 +1,29 @@ from __future__ import unicode_literals import frappe + def execute(): - invalid_selling_item_price = frappe.db.sql( - """SELECT name FROM `tabItem Price` WHERE selling = 1 and buying = 0 and (supplier IS NOT NULL or supplier = '')""" - ) - invalid_buying_item_price = frappe.db.sql( - """SELECT name FROM `tabItem Price` WHERE selling = 0 and buying = 1 and (customer IS NOT NULL or customer = '')""" - ) - docs_to_modify = invalid_buying_item_price + invalid_selling_item_price - for d in docs_to_modify: - # saving the doc will auto reset invalid customer/supplier field - doc = frappe.get_doc("Item Price", d[0]) - doc.save() \ No newline at end of file + """ + 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 + 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""") + + # 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""") diff --git a/erpnext/patches/v12_0/update_address_template_for_india.py b/erpnext/patches/v12_0/update_address_template_for_india.py new file mode 100644 index 0000000000..0d582da4b5 --- /dev/null +++ b/erpnext/patches/v12_0/update_address_template_for_india.py @@ -0,0 +1,12 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +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 not address_template or "gstin" not in address_template: + set_up_address_templates(default_country='India') diff --git a/erpnext/patches/v12_0/update_bom_in_so_mr.py b/erpnext/patches/v12_0/update_bom_in_so_mr.py new file mode 100644 index 0000000000..309ae4c2ab --- /dev/null +++ b/erpnext/patches/v12_0/update_bom_in_so_mr.py @@ -0,0 +1,19 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("stock", "doctype", "material_request_item") + frappe.reload_doc("selling", "doctype", "sales_order_item") + + for doctype in ["Sales Order", "Material Request"]: + condition = " and child_doc.stock_qty > child_doc.produced_qty" + 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 + SET + child_doc.bom_no = item.default_bom + WHERE + child_doc.item_code = item.name and child_doc.docstatus < 2 + and item.default_bom is not null and item.default_bom != '' {cond} + """.format(doc = doctype, cond = condition)) \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_item_tax_template_company.py b/erpnext/patches/v12_0/update_item_tax_template_company.py new file mode 100644 index 0000000000..f7496999b3 --- /dev/null +++ b/erpnext/patches/v12_0/update_item_tax_template_company.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + 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() \ No newline at end of file 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 new file mode 100644 index 0000000000..f5e7b947c2 --- /dev/null +++ b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py @@ -0,0 +1,31 @@ +from __future__ import unicode_literals +import frappe +from frappe.utils import getdate, flt +from erpnext.setup.utils import get_exchange_rate + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "bom") + frappe.reload_doc("manufacturing", "doctype", "bom_item") + + 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(""" + SELECT + bom.creation, bom.name, bom.price_list_currency as currency, + company.default_currency as company_currency + FROM + `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") + + frappe.db.set_value("BOM", d.name, "plc_conversion_rate", plc_conversion_rate) \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_uom_conversion_factor.py b/erpnext/patches/v12_0/update_uom_conversion_factor.py new file mode 100644 index 0000000000..b5a20aa6fd --- /dev/null +++ b/erpnext/patches/v12_0/update_uom_conversion_factor.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals +import frappe, json + +def execute(): + from erpnext.setup.setup_wizard.operations.install_fixtures import add_uom_data + + frappe.reload_doc("setup", "doctype", "UOM Conversion Factor") + frappe.reload_doc("setup", "doctype", "UOM") + frappe.reload_doc("stock", "doctype", "UOM Category") + + add_uom_data() \ No newline at end of file diff --git a/erpnext/patches/v13_0/__init__.py b/erpnext/patches/v13_0/__init__.py index e69de29bb2..baffc48825 100644 --- a/erpnext/patches/v13_0/__init__.py +++ b/erpnext/patches/v13_0/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py new file mode 100644 index 0000000000..9ad48e23b7 --- /dev/null +++ b/erpnext/patches/v13_0/check_is_income_tax_component.py @@ -0,0 +1,43 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe, erpnext +from erpnext.regional.india.setup import setup + +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' + ] + + for doctype in doctypes: + frappe.reload_doc('Payroll', 'doctype', doctype) + + + reports = ['Professional Tax Deductions', 'Provident Fund Deductions'] + for report in reports: + frappe.reload_doc('Regional', 'Report', report) + frappe.reload_doc('Regional', 'Report', report) + + if erpnext.get_region() == "India": + setup(patch=True) + + if frappe.db.exists("Salary Component", "Income Tax"): + frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1) + 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) + for component in components: + frappe.db.set_value("Salary Component", component.name, "is_income_tax_component", 1) + + if erpnext.get_region() == "India": + 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") \ No newline at end of file diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py new file mode 100644 index 0000000000..8bdc07ee5b --- /dev/null +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -0,0 +1,23 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +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"] + + for report in reports_to_delete: + if frappe.db.exists("Report", report): + delete_auto_email_reports(report) + + frappe.delete_doc("Report", report) + +def delete_auto_email_reports(report): + """ 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]) \ No newline at end of file diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py new file mode 100644 index 0000000000..0f44865808 --- /dev/null +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -0,0 +1,21 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + reports_to_delete = ["Ordered Items To Be Delivered", "Ordered Items To Be Billed"] + + for report in reports_to_delete: + if frappe.db.exists("Report", report): + delete_auto_email_reports(report) + + frappe.delete_doc("Report", report) + +def delete_auto_email_reports(report): + """ 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]) \ No newline at end of file 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 new file mode 100644 index 0000000000..4d7c85ce2d --- /dev/null +++ b/erpnext/patches/v13_0/move_doctype_reports_and_notification_from_hr_to_payroll.py @@ -0,0 +1,52 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + frappe.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';""" + ) + + 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) + + reports = ["Salary Register", "Bank Remittance"] + + 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 new file mode 100644 index 0000000000..a901064b88 --- /dev/null +++ b/erpnext/patches/v13_0/move_payroll_setting_separately_from_hr_settings.py @@ -0,0 +1,27 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + data = frappe.db.sql('''SELECT * + FROM `tabSingles` + WHERE + doctype = "HR Settings" + AND + field in ( + "encrypt_salary_slips_in_emails", + "email_salary_slip_to_employee", + "daily_wages_fraction_for_half_day", + "disable_rounded_total", + "include_holidays_in_total_working_days", + "max_working_hours_against_timesheet", + "payroll_based_on", + "password_policy" + ) + ''', as_dict=1) + + 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 ec94cd01d1..1a91d218ba 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 @@ -11,19 +11,25 @@ def execute(): return for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"): - frappe.reload_doc("hr", "doctype", doctype) + frappe.reload_doc("Payroll", "doctype", doctype) + 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(""" SELECT - name, start_date, end_date, standard_tax_exemption_amount + {0} FROM `tabPayroll Period` WHERE company=%s ORDER BY start_date DESC - """, 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") income_tax_slab.name = "Tax Slab:" + period.name @@ -36,7 +42,8 @@ def execute(): income_tax_slab.effective_from = period.start_date income_tax_slab.company = company.name income_tax_slab.allow_tax_exemption = 1 - income_tax_slab.standard_tax_exemption_amount = period.standard_tax_exemption_amount + if standard_tax_exemption_amount_exists: + income_tax_slab.standard_tax_exemption_amount = period.standard_tax_exemption_amount income_tax_slab.flags.ignore_mandatory = True income_tax_slab.submit() 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 ddcadcb4de..fde8f86470 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 @@ -6,8 +6,10 @@ def execute(): if not frappe.db.table_exists("Additional Salary"): return - for doctype in ("Additional Salary", "Leave Encashment", "Employee Incentive", "Salary Detail"): - frappe.reload_doc("hr", "doctype", doctype) + for doctype in ("Additional Salary", "Employee Incentive", "Salary Detail"): + frappe.reload_doc("Payroll", "doctype", doctype) + + frappe.reload_doc("hr", "doctype", "Leave Encashment") additional_salaries = frappe.get_all("Additional Salary", fields = ['name', "salary_slip", "type", "salary_component"], 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 new file mode 100644 index 0000000000..be5e30f307 --- /dev/null +++ b/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +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'] + 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))) 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 new file mode 100644 index 0000000000..adfa20e368 --- /dev/null +++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py @@ -0,0 +1,41 @@ + +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.utils import add_to_date + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "work_order") + 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"]) + }) + + for d in data: + doc = frappe.get_doc("Work Order", d.name) + doc.set_actual_dates() + 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) + + 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 + 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_deferred_settings.py b/erpnext/patches/v13_0/update_deferred_settings.py new file mode 100644 index 0000000000..a7d82077b7 --- /dev/null +++ b/erpnext/patches/v13_0/update_deferred_settings.py @@ -0,0 +1,11 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt +from __future__ import unicode_literals +import frappe + +def execute(): + 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() \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py new file mode 100644 index 0000000000..c156ba9577 --- /dev/null +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -0,0 +1,93 @@ +# Copyright (c) 2018, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +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']) + }) + + 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']) + 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 + }) + + priority_dict = {} + + for priority in priorities: + 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': + 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=['*']) + for entry in service_levels: + 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(""" + 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) + + priority_list = priority_dict.get(entry.name) + if priority_list: + 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.db_update() + sla.db_update() + + frappe.delete_doc_if_exists('DocType', 'Service Level') + + +def convert_to_seconds(value, unit): + seconds = 0 + if unit == "Hour": + seconds = value * 3600 + if unit == "Day": + seconds = value * 3600 * 24 + if unit == "Week": + seconds = value * 3600 * 24 * 7 + return seconds diff --git a/erpnext/patches/v4_0/create_custom_fields_for_india_specific_fields.py b/erpnext/patches/v4_0/create_custom_fields_for_india_specific_fields.py index 9c602a3f78..fe50e444b5 100644 --- a/erpnext/patches/v4_0/create_custom_fields_for_india_specific_fields.py +++ b/erpnext/patches/v4_0/create_custom_fields_for_india_specific_fields.py @@ -8,7 +8,7 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field_ def execute(): frappe.reload_doc("stock", "doctype", "purchase_receipt") frappe.reload_doc("hr", "doctype", "employee") - frappe.reload_doc("hr", "doctype", "salary_slip") + frappe.reload_doc("Payroll", "doctype", "salary_slip") india_specific_fields = { "Purchase Receipt": [{ diff --git a/erpnext/patches/v5_0/rename_table_fieldnames.py b/erpnext/patches/v5_0/rename_table_fieldnames.py index 59f534303f..aefb0a2037 100644 --- a/erpnext/patches/v5_0/rename_table_fieldnames.py +++ b/erpnext/patches/v5_0/rename_table_fieldnames.py @@ -220,7 +220,7 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", "work_order_operation") frappe.reload_doc("manufacturing", "doctype", "workstation_working_hour") frappe.reload_doc("stock", "doctype", "item_variant") - frappe.reload_doc("hr", "doctype", "salary_detail") + frappe.reload_doc("Payroll", "doctype", "salary_detail") frappe.reload_doc("accounts", "doctype", "party_account") frappe.reload_doc("accounts", "doctype", "fiscal_year_company") diff --git a/erpnext/patches/v7_0/rename_salary_components.py b/erpnext/patches/v7_0/rename_salary_components.py index bc48e34317..1693f3bdf1 100644 --- a/erpnext/patches/v7_0/rename_salary_components.py +++ b/erpnext/patches/v7_0/rename_salary_components.py @@ -6,8 +6,8 @@ def execute(): if not frappe.db.exists("DocType", "Salary Structure Earning"): return - frappe.reload_doc("hr", "doctype", "salary_detail") - frappe.reload_doc("hr", "doctype", "salary_component") + frappe.reload_doc("Payroll", "doctype", "salary_detail") + frappe.reload_doc("Payroll", "doctype", "salary_component") standard_cols = ["name", "creation", "modified", "owner", "modified_by", "parent", "parenttype", "parentfield", "idx"] diff --git a/erpnext/patches/v7_0/update_mins_to_first_response.py b/erpnext/patches/v7_0/update_mins_to_first_response.py index 1df4b42ced..16681357e6 100644 --- a/erpnext/patches/v7_0/update_mins_to_first_response.py +++ b/erpnext/patches/v7_0/update_mins_to_first_response.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import frappe -from frappe.core.doctype.communication.email import update_mins_to_first_communication +from frappe.core.doctype.communication.communication import update_mins_to_first_communication def execute(): frappe.reload_doctype('Issue') diff --git a/erpnext/patches/v7_1/update_component_type.py b/erpnext/patches/v7_1/update_component_type.py index 552fc89467..24ca0570e0 100644 --- a/erpnext/patches/v7_1/update_component_type.py +++ b/erpnext/patches/v7_1/update_component_type.py @@ -3,7 +3,7 @@ import frappe from frappe.utils import flt def execute(): - frappe.reload_doc('hr', 'doctype', 'salary_component') + frappe.reload_doc('Payroll', 'doctype', 'salary_component') sal_components = frappe.db.sql(""" select DISTINCT salary_component, parentfield from `tabSalary Detail`""", as_dict=True) diff --git a/erpnext/patches/v7_1/update_missing_salary_component_type.py b/erpnext/patches/v7_1/update_missing_salary_component_type.py index 7d50ee4dff..824f2b881f 100644 --- a/erpnext/patches/v7_1/update_missing_salary_component_type.py +++ b/erpnext/patches/v7_1/update_missing_salary_component_type.py @@ -10,12 +10,12 @@ earnings or deductions in existing salary slips def execute(): frappe.reload_doc("accounts", "doctype", "salary_component_account") - frappe.reload_doc("hr", "doctype", "salary_component") - frappe.reload_doc("hr", "doctype", "taxable_salary_slab") - - for s in frappe.db.sql('''select name, type, salary_component_abbr from `tabSalary Component` + frappe.reload_doc("Payroll", "doctype", "salary_component") + frappe.reload_doc("Payroll", "doctype", "taxable_salary_slab") + + for s in frappe.db.sql('''select name, type, salary_component_abbr from `tabSalary Component` where ifnull(type, "")="" or ifnull(salary_component_abbr, "") = ""''', as_dict=1): - + component = frappe.get_doc('Salary Component', s.name) # guess @@ -29,22 +29,22 @@ def execute(): else: component.type = 'Deduction' - + if not s.salary_component_abbr: abbr = ''.join([c[0] for c in component.salary_component.split()]).upper() - + abbr_count = frappe.db.sql(""" - select - count(name) - from - `tabSalary Component` - where + select + count(name) + from + `tabSalary Component` + where salary_component_abbr = %s or salary_component_abbr like %s """, (abbr, abbr + "-%%")) - + if abbr_count and abbr_count[0][0] > 0: abbr = abbr + "-" + cstr(abbr_count[0][0]) - + component.salary_component_abbr = abbr - + component.save() diff --git a/erpnext/patches/v7_2/arrear_leave_encashment_as_salary_component.py b/erpnext/patches/v7_2/arrear_leave_encashment_as_salary_component.py index 3b9642dd3b..d2583b9422 100644 --- a/erpnext/patches/v7_2/arrear_leave_encashment_as_salary_component.py +++ b/erpnext/patches/v7_2/arrear_leave_encashment_as_salary_component.py @@ -2,7 +2,9 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doctype('Salary Slip', 'Salary Component') + # frappe.reload_doctype('Salary Slip', 'Salary Component') + frappe.reload_doc("Payroll", "doctype", "Salary Slip") + frappe.reload_doc("Payroll", "doctype", "Salary Component") salary_components = [['Arrear', "ARR"], ['Leave Encashment', 'LENC']] for salary_component, salary_abbr in salary_components: if not frappe.db.exists('Salary Component', salary_component): diff --git a/erpnext/patches/v7_2/update_abbr_in_salary_slips.py b/erpnext/patches/v7_2/update_abbr_in_salary_slips.py index 19dcb5e3b2..57432fe986 100644 --- a/erpnext/patches/v7_2/update_abbr_in_salary_slips.py +++ b/erpnext/patches/v7_2/update_abbr_in_salary_slips.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doctype('Salary Slip') + frappe.reload_doc('Payroll', 'doctype', 'Salary Slip') if not frappe.db.has_column('Salary Detail', 'abbr'): return diff --git a/erpnext/patches/v7_2/update_salary_slips.py b/erpnext/patches/v7_2/update_salary_slips.py index 11a52f9587..9fcce62d8f 100644 --- a/erpnext/patches/v7_2/update_salary_slips.py +++ b/erpnext/patches/v7_2/update_salary_slips.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals import frappe -from erpnext.hr.doctype.payroll_entry.payroll_entry import get_month_details +from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from frappe.utils import cint def execute(): - frappe.reload_doctype('Salary Slip') + frappe.reload_doc("Payroll", "doctype", "Salary Slip") if not frappe.db.has_column('Salary Slip', 'fiscal_year'): return diff --git a/erpnext/patches/v8_7/sync_india_custom_fields.py b/erpnext/patches/v8_7/sync_india_custom_fields.py index 73e1b182e8..eb24a90f01 100644 --- a/erpnext/patches/v8_7/sync_india_custom_fields.py +++ b/erpnext/patches/v8_7/sync_india_custom_fields.py @@ -7,11 +7,11 @@ def execute(): if not company: return - frappe.reload_doc('hr', 'doctype', 'payroll_period') - frappe.reload_doc('hr', 'doctype', 'employee_tax_exemption_declaration') - frappe.reload_doc('hr', 'doctype', 'employee_tax_exemption_proof_submission') - frappe.reload_doc('hr', 'doctype', 'employee_tax_exemption_declaration_category') - frappe.reload_doc('hr', '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') diff --git a/erpnext/patches/v9_0/update_employee_loan_details.py b/erpnext/patches/v9_0/update_employee_loan_details.py index 86690fc1f8..ef8d32855f 100644 --- a/erpnext/patches/v9_0/update_employee_loan_details.py +++ b/erpnext/patches/v9_0/update_employee_loan_details.py @@ -5,8 +5,8 @@ from __future__ import unicode_literals import frappe def execute(): - frappe.reload_doc('hr', 'doctype', 'salary_slip_loan') - frappe.reload_doc('hr', 'doctype', 'salary_slip') + frappe.reload_doc('Payroll', 'doctype', 'salary_slip_loan') + frappe.reload_doc('Payroll', 'doctype', 'salary_slip') for data in frappe.db.sql(""" select name, start_date, end_date, total_loan_repayment diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py b/erpnext/payroll/__init__.py similarity index 100% rename from erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py rename to erpnext/payroll/__init__.py diff --git a/erpnext/payroll/dashboard_fixtures.py b/erpnext/payroll/dashboard_fixtures.py new file mode 100644 index 0000000000..ae7a9ff51a --- /dev/null +++ b/erpnext/payroll/dashboard_fixtures.py @@ -0,0 +1,100 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import erpnext +from erpnext.hr.dashboard_fixtures import get_dashboards_chart_doc, get_number_cards_doc +import json +from frappe import _ + +def get_data(): + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(), + "number_cards": get_number_cards(), + }) + +def get_dashboards(): + dashboards = [] + dashboards.append(get_payroll_dashboard()) + return dashboards + +def get_payroll_dashboard(): + return { + "name": "Payroll", + "dashboard_name": "Payroll", + "is_default": 1, + "charts": [ + { "chart": "Outgoing Salary", "width": "Full"}, + { "chart": "Designation Wise Salary(Last Month)", "width": "Half"}, + { "chart": "Department Wise Salary(Last Month)", "width": "Half"}, + ], + "cards": [ + {"card": "Total Declaration Submitted"}, + {"card": "Total Salary Structure"}, + {"card": "Total Incentive Given(Last month)"}, + {"card": "Total Outgoing Salary(Last month)"}, + ] + } + +def get_charts(): + dashboard_charts= [ + get_dashboards_chart_doc('Outgoing Salary', "Sum", "Line", + document_type = "Salary Slip", based_on="end_date", + value_based_on = "rounded_total", time_interval = "Monthly", timeseries = 1, + filters_json = json.dumps([["Salary Slip", "docstatus", "=", 1]])) + ] + + dashboard_charts.append( + get_dashboards_chart_doc('Department Wise Salary(Last Month)', "Group By", "Bar", + document_type = "Salary Slip", group_by_type="Sum", group_by_based_on="department", + time_interval = "Monthly", aggregate_function_based_on = "rounded_total", + filters_json = json.dumps([ + ["Salary Slip", "docstatus", "=", 1], + ["Salary Slip", "start_date", "Previous","1 month"] + ]) + ) + ) + + dashboard_charts.append( + get_dashboards_chart_doc('Designation Wise Salary(Last Month)', "Group By", "Bar", + document_type = "Salary Slip", group_by_type="Sum", group_by_based_on="designation", + time_interval = "Monthly", aggregate_function_based_on = "rounded_total", + filters_json = json.dumps([ + ["Salary Slip", "docstatus", "=", 1], + ["Salary Slip", "start_date", "Previous","1 month"] + ]) + ) + ) + + return dashboard_charts + +def get_number_cards(): + number_cards = [get_number_cards_doc("Employee Tax Exemption Declaration", "Total Declaration Submitted", filters_json = json.dumps([ + ["Employee Tax Exemption Declaration", "docstatus", "=","1"], + ["Employee Tax Exemption Declaration","creation","Previous","1 year"] + ]) + )] + + number_cards.append(get_number_cards_doc("Employee Incentive", "Total Incentive Given(Last month)", + time_interval = "Monthly", func = "Sum", aggregate_function_based_on = "incentive_amount", + filters_json = json.dumps([ + ["Employee Incentive", "docstatus", "=", 1], + ["Employee Incentive","payroll_date","Previous","1 year"] + ])) + ) + + number_cards.append(get_number_cards_doc("Salary Slip", "Total Outgoing Salary(Last month)", + time_interval = "Monthly", time_span= "Monthly", func = "Sum", aggregate_function_based_on = "rounded_total", + filters_json = json.dumps([ + ["Salary Slip", "docstatus", "=", 1], + ["Salary Slip", "start_date","Previous","1 month"] + ])) + ) + number_cards.append(get_number_cards_doc("Salary Structure", "Total Salary Structure", + filters_json = json.dumps([ + ["Salary Structure", "docstatus", "=", 1] + ])) + ) + + return number_cards \ No newline at end of file diff --git a/erpnext/payroll/desk_page/payroll/payroll.json b/erpnext/payroll/desk_page/payroll/payroll.json new file mode 100644 index 0000000000..b5eac465c8 --- /dev/null +++ b/erpnext/payroll/desk_page/payroll/payroll.json @@ -0,0 +1,84 @@ +{ + "cards": [ + { + "hidden": 0, + "label": "Payroll", + "links": "[\n {\n \"label\": \"Salary Component\",\n \"name\": \"Salary Component\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Salary Structure\",\n \"name\": \"Salary Structure\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Structure Assignment\",\n \"name\": \"Salary Structure Assignment\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Payroll Entry\",\n \"name\": \"Payroll Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Slip\",\n \"name\": \"Salary Slip\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Taxation", + "links": "[\n {\n \"label\": \"Payroll Period\",\n \"name\": \"Payroll Period\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Income Tax Slab\",\n \"name\": \"Income Tax Slab\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Declaration\",\n \"name\": \"Employee Tax Exemption Declaration\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Proof Submission\",\n \"name\": \"Employee Tax Exemption Proof Submission\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Category\",\n \"name\": \"Employee Tax Exemption Category\",\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Sub Category\",\n \"name\": \"Employee Tax Exemption Sub Category\",\n \"type\": \"doctype\"\n \n }\n]" + }, + { + "hidden": 0, + "label": "Compensations", + "links": "[\n {\n \"label\": \"Additional Salary\",\n \"name\": \"Additional Salary\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Retention Bonus\",\n \"name\": \"Retention Bonus\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Incentive\",\n \"name\": \"Employee Incentive\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Benefit Application\",\n \"name\": \"Employee Benefit Application\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Benefit Claim\",\n \"name\": \"Employee Benefit Claim\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Reports", + "links": "[\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"is_query_report\": true,\n \"label\": \"Salary Register\",\n \"name\": \"Salary Register\",\n \"type\": \"report\"\n \n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"label\": \"Salary Payments Based On Payment Mode\",\n \"is_query_report\": true,\n \"name\": \"Salary Payments Based On Payment Mode\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"label\": \"Salary Payments via ECS\",\n \"is_query_report\": true,\n \"name\": \"Salary Payments via ECS\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"label\": \"Income Tax Deductions\",\n \"is_query_report\": true,\n \"name\": \"Income Tax Deductions\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"label\": \"Professional Tax Deductions\",\n \"is_query_report\": true,\n \"name\": \"Professional Tax Deductions\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"label\": \"Provident Fund Deductions\",\n \"is_query_report\": true,\n \"name\": \"Provident Fund Deductions\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Payroll Entry\"\n ],\n \"doctype\": \"Payroll Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Remittance\",\n \"name\": \"Bank Remittance\",\n \"type\": \"report\"\n \n }\n]" + } + ], + "category": "Modules", + "charts": [ + { + "chart_name": "Outgoing Salary", + "label": "Outgoing Salary" + } + ], + "creation": "2020-05-27 19:54:23.405607", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Desk Page", + "extends_another_page": 0, + "hide_custom": 0, + "idx": 0, + "is_standard": 1, + "label": "Payroll", + "modified": "2020-06-19 12:23:06.034046", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Payroll", + "onboarding": "Payroll", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [ + { + "label": "Salary Structure", + "link_to": "Salary Structure", + "type": "DocType" + }, + { + "label": "Payroll Entry", + "link_to": "Payroll Entry", + "type": "DocType" + }, + { + "color": "", + "format": "{} Pending", + "label": "Salary Slip", + "link_to": "Salary Slip", + "stats_filter": "{\"status\": \"Draft\"}", + "type": "DocType" + }, + { + "label": "Income Tax Slab", + "link_to": "Income Tax Slab", + "type": "DocType" + }, + { + "label": "Salary Register", + "link_to": "Salary Register", + "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "Payroll", + "type": "Dashboard" + } + ] +} \ No newline at end of file diff --git a/erpnext/hr/doctype/payroll_employee_detail/__init__.py b/erpnext/payroll/doctype/__init__.py similarity index 100% rename from erpnext/hr/doctype/payroll_employee_detail/__init__.py rename to erpnext/payroll/doctype/__init__.py diff --git a/erpnext/hr/doctype/payroll_entry/__init__.py b/erpnext/payroll/doctype/additional_salary/__init__.py similarity index 100% rename from erpnext/hr/doctype/payroll_entry/__init__.py rename to erpnext/payroll/doctype/additional_salary/__init__.py diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js similarity index 88% rename from erpnext/hr/doctype/additional_salary/additional_salary.js rename to erpnext/payroll/doctype/additional_salary/additional_salary.js index fb42b6f410..d56cd4e967 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -8,8 +8,7 @@ frappe.ui.form.on('Additional Salary', { frm.set_query("employee", function() { return { filters: { - company: frm.doc.company, - status: "Active" + company: frm.doc.company } }; }); diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.json b/erpnext/payroll/doctype/additional_salary/additional_salary.json similarity index 98% rename from erpnext/hr/doctype/additional_salary/additional_salary.json rename to erpnext/payroll/doctype/additional_salary/additional_salary.json index bfb543f49a..69cb5da893 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.json +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.json @@ -146,7 +146,7 @@ "label": "To Date", "mandatory_depends_on": "eval:(doc.is_recurring==1)" }, - { + { "fieldname": "ref_doctype", "fieldtype": "Link", "label": "Reference Document Type", @@ -163,9 +163,9 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-04 18:06:29.170878", + "modified": "2020-06-22 21:10:50.374063", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Additional Salary", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py similarity index 89% rename from erpnext/hr/doctype/additional_salary/additional_salary.py rename to erpnext/payroll/doctype/additional_salary/additional_salary.py index bab6fb545f..ef174bdea2 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -33,12 +33,16 @@ class AdditionalSalary(Document): frappe.throw(_("From Date can not be greater than To Date.")) if date_of_joining: - if getdate(self.payroll_date) < getdate(date_of_joining): + if self.payroll_date and getdate(self.payroll_date) < getdate(date_of_joining): frappe.throw(_("Payroll date can not be less than employee's joining date.")) - elif getdate(self.from_date) < getdate(date_of_joining): + elif self.from_date and getdate(self.from_date) < getdate(date_of_joining): frappe.throw(_("From date can not be less than employee's joining date.")) - elif getdate(self.to_date) > getdate(relieving_date): + + if relieving_date: + if self.to_date and getdate(self.to_date) > getdate(relieving_date): frappe.throw(_("To date can not be greater than employee's relieving date.")) + if self.payroll_date and getdate(self.payroll_date) > getdate(relieving_date): + frappe.throw(_("Payroll date can not be greater than employee's relieving date.")) def get_amount(self, sal_start_date, sal_end_date): start_date = getdate(sal_start_date) @@ -107,4 +111,4 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty existing_salary_components.append(d.salary_component) - return salary_components_details, additional_salary_details \ No newline at end of file + return salary_components_details, additional_salary_details diff --git a/erpnext/hr/doctype/additional_salary/test_additional_salary.js b/erpnext/payroll/doctype/additional_salary/test_additional_salary.js similarity index 100% rename from erpnext/hr/doctype/additional_salary/test_additional_salary.js rename to erpnext/payroll/doctype/additional_salary/test_additional_salary.js diff --git a/erpnext/hr/doctype/additional_salary/test_additional_salary.py b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py similarity index 87% rename from erpnext/hr/doctype/additional_salary/test_additional_salary.py rename to erpnext/payroll/doctype/additional_salary/test_additional_salary.py index 6f93fb5df8..de26543b57 100644 --- a/erpnext/hr/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py @@ -6,8 +6,8 @@ import unittest import frappe, erpnext from frappe.utils import nowdate, add_days from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.salary_component.test_salary_component import create_salary_component -from erpnext.hr.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, setup_test +from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component +from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, setup_test class TestAdditionalSalary(unittest.TestCase): diff --git a/erpnext/hr/doctype/payroll_period/__init__.py b/erpnext/payroll/doctype/employee_benefit_application/__init__.py similarity index 100% rename from erpnext/hr/doctype/payroll_period/__init__.py rename to erpnext/payroll/doctype/employee_benefit_application/__init__.py diff --git a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js similarity index 83% rename from erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js rename to erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js index b73dcf8ac3..f509df31e8 100644 --- a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.js +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.js @@ -6,7 +6,7 @@ frappe.ui.form.on('Employee Benefit Application', { frm.trigger('set_earning_component'); var method, args; if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ - method = "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; + method = "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; args = { employee: frm.doc.employee, on_date: frm.doc.date, @@ -15,7 +15,7 @@ frappe.ui.form.on('Employee Benefit Application', { get_max_benefits(frm, method, args); } else if(frm.doc.employee && frm.doc.date){ - method = "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits"; + method = "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits"; args = { employee: frm.doc.employee, on_date: frm.doc.date @@ -32,7 +32,7 @@ frappe.ui.form.on('Employee Benefit Application', { if(!frm.doc.employee && !frm.doc.date) return; frm.set_query("earning_component", "employee_benefits", function() { return { - query : "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_earning_components", + query : "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_earning_components", filters: {date: frm.doc.date, employee: frm.doc.employee} }; }); @@ -41,7 +41,7 @@ frappe.ui.form.on('Employee Benefit Application', { payroll_period: function(frm) { var method, args; if(frm.doc.employee && frm.doc.date && frm.doc.payroll_period){ - method = "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; + method = "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_max_benefits_remaining"; args = { employee: frm.doc.employee, on_date: frm.doc.date, diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json new file mode 100644 index 0000000000..b0c1bd6c3e --- /dev/null +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json @@ -0,0 +1,191 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "HR-BEN-APP-.YY.-.MM.-.#####", + "creation": "2018-04-13 16:31:39.190787", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "max_benefits", + "remaining_benefit", + "column_break_2", + "date", + "payroll_period", + "department", + "amended_from", + "section_break_4", + "employee_benefits", + "totals", + "total_amount", + "pro_rata_dispensed_amount" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "max_benefits", + "fieldtype": "Currency", + "label": "Max Benefits (Yearly)", + "read_only": 1 + }, + { + "fieldname": "remaining_benefit", + "fieldtype": "Currency", + "label": "Remaining Benefits (Yearly)", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "date", + "fieldtype": "Date", + "label": "Date", + "reqd": 1 + }, + { + "fieldname": "payroll_period", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payroll Period", + "options": "Payroll Period", + "reqd": 1 + }, + { + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Benefit Application", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Benefits Applied" + }, + { + "fieldname": "employee_benefits", + "fieldtype": "Table", + "label": "Employee Benefits", + "options": "Employee Benefit Application Detail", + "reqd": 1 + }, + { + "fieldname": "totals", + "fieldtype": "Section Break", + "label": "Totals" + }, + { + "fieldname": "total_amount", + "fieldtype": "Currency", + "label": "Total Amount", + "read_only": 1 + }, + { + "fieldname": "pro_rata_dispensed_amount", + "fieldtype": "Currency", + "label": "Dispensed Amount (Pro-rated)", + "read_only": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-06-22 22:58:31.271922", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Employee Benefit Application", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "employee_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py similarity index 97% rename from erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.py rename to erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index feaa92590a..d7d00e6480 100644 --- a/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -7,8 +7,8 @@ import frappe from frappe import _ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt from frappe.model.document import Document -from erpnext.hr.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor -from erpnext.hr.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure +from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor +from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount class EmployeeBenefitApplication(Document): @@ -222,7 +222,7 @@ def get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit): return benefit_amount - +@frappe.whitelist() def get_earning_components(doctype, txt, searchfield, start, page_len, filters): if len(filters) < 2: return {} diff --git a/erpnext/hr/doctype/employee_benefit_application/test_employee_benefit_application.js b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.js similarity index 100% rename from erpnext/hr/doctype/employee_benefit_application/test_employee_benefit_application.js rename to erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.js diff --git a/erpnext/hr/doctype/employee_benefit_application/test_employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py similarity index 100% rename from erpnext/hr/doctype/employee_benefit_application/test_employee_benefit_application.py rename to erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py diff --git a/erpnext/hr/doctype/payroll_period_date/__init__.py b/erpnext/payroll/doctype/employee_benefit_application_detail/__init__.py similarity index 100% rename from erpnext/hr/doctype/payroll_period_date/__init__.py rename to erpnext/payroll/doctype/employee_benefit_application_detail/__init__.py diff --git a/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json b/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json new file mode 100644 index 0000000000..fa6b4da2af --- /dev/null +++ b/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.json @@ -0,0 +1,58 @@ +{ + "actions": [], + "creation": "2018-04-13 16:36:18.389786", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "earning_component", + "pay_against_benefit_claim", + "max_benefit_amount", + "amount" + ], + "fields": [ + { + "fieldname": "earning_component", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Earning Component", + "options": "Salary Component", + "reqd": 1 + }, + { + "default": "0", + "fetch_from": "earning_component.pay_against_benefit_claim", + "fieldname": "pay_against_benefit_claim", + "fieldtype": "Check", + "label": "Pay Against Benefit Claim", + "read_only": 1 + }, + { + "fetch_from": "earning_component.max_benefit_amount", + "fieldname": "max_benefit_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Max Benefit Amount", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-22 23:45:00.519134", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Employee Benefit Application Detail", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_benefit_application_detail/employee_benefit_application_detail.py b/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.py similarity index 73% rename from erpnext/hr/doctype/employee_benefit_application_detail/employee_benefit_application_detail.py rename to erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.py index 3a502fe524..65405feaf1 100644 --- a/erpnext/hr/doctype/employee_benefit_application_detail/employee_benefit_application_detail.py +++ b/erpnext/payroll/doctype/employee_benefit_application_detail/employee_benefit_application_detail.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class EmployeeBenefitApplicationDetail(Document): diff --git a/erpnext/hr/doctype/retention_bonus/__init__.py b/erpnext/payroll/doctype/employee_benefit_claim/__init__.py similarity index 100% rename from erpnext/hr/doctype/retention_bonus/__init__.py rename to erpnext/payroll/doctype/employee_benefit_claim/__init__.py diff --git a/erpnext/hr/doctype/employee_benefit_claim/employee_benefit_claim.js b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js similarity index 77% rename from erpnext/hr/doctype/employee_benefit_claim/employee_benefit_claim.js rename to erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js index 5e12828ba4..6db6cb86b3 100644 --- a/erpnext/hr/doctype/employee_benefit_claim/employee_benefit_claim.js +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js @@ -5,7 +5,7 @@ frappe.ui.form.on('Employee Benefit Claim', { setup: function(frm) { frm.set_query("earning_component", function() { return { - query : "erpnext.hr.doctype.employee_benefit_application.employee_benefit_application.get_earning_components", + query : "erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application.get_earning_components", filters: {date: frm.doc.claim_date, employee: frm.doc.employee} }; }); diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json new file mode 100644 index 0000000000..ae4c218615 --- /dev/null +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json @@ -0,0 +1,194 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "HR-BEN-CLM-.YY.-.MM.-.#####", + "creation": "2018-04-13 16:43:10.386409", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "department", + "column_break_3", + "claim_date", + "benefit_type_and_amount", + "earning_component", + "max_amount_eligible", + "pay_against_benefit_claim", + "claimed_amount", + "salary_slip", + "amended_from", + "section_break_9", + "attachments" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "claim_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Claim Date", + "reqd": 1 + }, + { + "fieldname": "benefit_type_and_amount", + "fieldtype": "Section Break", + "label": "Benefit Type and Amount" + }, + { + "fieldname": "earning_component", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Claim Benefit For", + "options": "Salary Component", + "reqd": 1 + }, + { + "fetch_from": "earning_component.max_benefit_amount", + "fieldname": "max_amount_eligible", + "fieldtype": "Currency", + "label": "Max Amount Eligible", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "earning_component.pay_against_benefit_claim", + "fieldname": "pay_against_benefit_claim", + "fieldtype": "Check", + "hidden": 1, + "label": "Pay Against Benefit Claim", + "read_only": 1 + }, + { + "fieldname": "claimed_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Claimed Amount", + "reqd": 1 + }, + { + "fieldname": "salary_slip", + "fieldtype": "Link", + "label": "Salary Slip", + "options": "Salary Slip", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Benefit Claim", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Expense Proof" + }, + { + "fieldname": "attachments", + "fieldtype": "Attach", + "label": "Attachments" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-06-22 23:01:50.791676", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Employee Benefit Claim", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "employee_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_benefit_claim/employee_benefit_claim.py b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py similarity index 96% rename from erpnext/hr/doctype/employee_benefit_claim/employee_benefit_claim.py rename to erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py index 3a12c9c9f9..d9937a7bb9 100644 --- a/erpnext/hr/doctype/employee_benefit_claim/employee_benefit_claim.py +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py @@ -7,10 +7,10 @@ import frappe from frappe import _ from frappe.utils import flt from frappe.model.document import Document -from erpnext.hr.doctype.employee_benefit_application.employee_benefit_application import get_max_benefits +from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_max_benefits from erpnext.hr.utils import get_previous_claimed_amount -from erpnext.hr.doctype.payroll_period.payroll_period import get_payroll_period -from erpnext.hr.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure +from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period +from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure class EmployeeBenefitClaim(Document): def validate(self): diff --git a/erpnext/hr/doctype/employee_benefit_claim/test_employee_benefit_claim.js b/erpnext/payroll/doctype/employee_benefit_claim/test_employee_benefit_claim.js similarity index 100% rename from erpnext/hr/doctype/employee_benefit_claim/test_employee_benefit_claim.js rename to erpnext/payroll/doctype/employee_benefit_claim/test_employee_benefit_claim.js diff --git a/erpnext/hr/doctype/employee_benefit_claim/test_employee_benefit_claim.py b/erpnext/payroll/doctype/employee_benefit_claim/test_employee_benefit_claim.py similarity index 100% rename from erpnext/hr/doctype/employee_benefit_claim/test_employee_benefit_claim.py rename to erpnext/payroll/doctype/employee_benefit_claim/test_employee_benefit_claim.py diff --git a/erpnext/hr/doctype/salary_component/__init__.py b/erpnext/payroll/doctype/employee_incentive/__init__.py similarity index 100% rename from erpnext/hr/doctype/salary_component/__init__.py rename to erpnext/payroll/doctype/employee_incentive/__init__.py diff --git a/erpnext/hr/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js similarity index 100% rename from erpnext/hr/doctype/employee_incentive/employee_incentive.js rename to erpnext/payroll/doctype/employee_incentive/employee_incentive.js diff --git a/erpnext/hr/doctype/employee_incentive/employee_incentive.json b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json similarity index 97% rename from erpnext/hr/doctype/employee_incentive/employee_incentive.json rename to erpnext/payroll/doctype/employee_incentive/employee_incentive.json index e2d8a11f47..204c9a40b1 100644 --- a/erpnext/hr/doctype/employee_incentive/employee_incentive.json +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json @@ -74,9 +74,9 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-05 18:59:40.526014", + "modified": "2020-06-22 22:42:51.209630", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Employee Incentive", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/employee_incentive/employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py similarity index 93% rename from erpnext/hr/doctype/employee_incentive/employee_incentive.py rename to erpnext/payroll/doctype/employee_incentive/employee_incentive.py index 44763fc077..84a97f6bb2 100644 --- a/erpnext/hr/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py @@ -13,6 +13,7 @@ class EmployeeIncentive(Document): 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 additional_salary.amount = self.incentive_amount additional_salary.payroll_date = self.payroll_date additional_salary.company = company diff --git a/erpnext/hr/doctype/employee_incentive/test_employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/test_employee_incentive.js similarity index 100% rename from erpnext/hr/doctype/employee_incentive/test_employee_incentive.js rename to erpnext/payroll/doctype/employee_incentive/test_employee_incentive.js diff --git a/erpnext/hr/doctype/employee_incentive/test_employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/test_employee_incentive.py similarity index 100% rename from erpnext/hr/doctype/employee_incentive/test_employee_incentive.py rename to erpnext/payroll/doctype/employee_incentive/test_employee_incentive.py diff --git a/erpnext/hr/doctype/salary_detail/__init__.py b/erpnext/payroll/doctype/employee_other_income/__init__.py similarity index 100% rename from erpnext/hr/doctype/salary_detail/__init__.py rename to erpnext/payroll/doctype/employee_other_income/__init__.py diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.js b/erpnext/payroll/doctype/employee_other_income/employee_other_income.js similarity index 100% rename from erpnext/hr/doctype/employee_other_income/employee_other_income.js rename to erpnext/payroll/doctype/employee_other_income/employee_other_income.js diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.json b/erpnext/payroll/doctype/employee_other_income/employee_other_income.json similarity index 97% rename from erpnext/hr/doctype/employee_other_income/employee_other_income.json rename to erpnext/payroll/doctype/employee_other_income/employee_other_income.json index 8abfe1e93a..14f63e4fdd 100644 --- a/erpnext/hr/doctype/employee_other_income/employee_other_income.json +++ b/erpnext/payroll/doctype/employee_other_income/employee_other_income.json @@ -76,9 +76,9 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-05-14 17:17:38.883126", + "modified": "2020-06-22 22:55:17.604688", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Employee Other Income", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.py b/erpnext/payroll/doctype/employee_other_income/employee_other_income.py similarity index 100% rename from erpnext/hr/doctype/employee_other_income/employee_other_income.py rename to erpnext/payroll/doctype/employee_other_income/employee_other_income.py diff --git a/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py b/erpnext/payroll/doctype/employee_other_income/test_employee_other_income.py similarity index 100% rename from erpnext/hr/doctype/employee_other_income/test_employee_other_income.py rename to erpnext/payroll/doctype/employee_other_income/test_employee_other_income.py diff --git a/erpnext/hr/doctype/salary_slip_timesheet/__init__.py b/erpnext/payroll/doctype/employee_tax_exemption_category/__init__.py similarity index 100% rename from erpnext/hr/doctype/salary_slip_timesheet/__init__.py rename to erpnext/payroll/doctype/employee_tax_exemption_category/__init__.py diff --git a/erpnext/hr/doctype/employee_tax_exemption_category/employee_tax_exemption_category.js b/erpnext/payroll/doctype/employee_tax_exemption_category/employee_tax_exemption_category.js similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_category/employee_tax_exemption_category.js rename to erpnext/payroll/doctype/employee_tax_exemption_category/employee_tax_exemption_category.js diff --git a/erpnext/payroll/doctype/employee_tax_exemption_category/employee_tax_exemption_category.json b/erpnext/payroll/doctype/employee_tax_exemption_category/employee_tax_exemption_category.json new file mode 100644 index 0000000000..f2556d7d96 --- /dev/null +++ b/erpnext/payroll/doctype/employee_tax_exemption_category/employee_tax_exemption_category.json @@ -0,0 +1,74 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2018-04-13 16:51:36.971140", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "max_amount", + "is_active" + ], + "fields": [ + { + "fieldname": "max_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Max Exemption Amount" + }, + { + "default": "1", + "fieldname": "is_active", + "fieldtype": "Check", + "label": "Is Active" + } + ], + "links": [], + "modified": "2020-06-22 23:16:47.472910", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Employee Tax Exemption Category", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_category/employee_tax_exemption_category.py b/erpnext/payroll/doctype/employee_tax_exemption_category/employee_tax_exemption_category.py similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_category/employee_tax_exemption_category.py rename to erpnext/payroll/doctype/employee_tax_exemption_category/employee_tax_exemption_category.py diff --git a/erpnext/hr/doctype/employee_tax_exemption_category/test_employee_tax_exemption_category.js b/erpnext/payroll/doctype/employee_tax_exemption_category/test_employee_tax_exemption_category.js similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_category/test_employee_tax_exemption_category.js rename to erpnext/payroll/doctype/employee_tax_exemption_category/test_employee_tax_exemption_category.js diff --git a/erpnext/hr/doctype/employee_tax_exemption_category/test_employee_tax_exemption_category.py b/erpnext/payroll/doctype/employee_tax_exemption_category/test_employee_tax_exemption_category.py similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_category/test_employee_tax_exemption_category.py rename to erpnext/payroll/doctype/employee_tax_exemption_category/test_employee_tax_exemption_category.py diff --git a/erpnext/hr/doctype/salary_structure_assignment/__init__.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/__init__.py similarity index 100% rename from erpnext/hr/doctype/salary_structure_assignment/__init__.py rename to erpnext/payroll/doctype/employee_tax_exemption_declaration/__init__.py diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js similarity index 88% rename from erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js rename to erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js index a827eca1c4..0e0c9b5a1a 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js @@ -42,7 +42,7 @@ frappe.ui.form.on('Employee Tax Exemption Declaration', { if(frm.doc.docstatus==1) { frm.add_custom_button(__('Submit Proof'), function() { frappe.model.open_mapped_doc({ - method: "erpnext.hr.doctype.employee_tax_exemption_declaration.employee_tax_exemption_declaration.make_proof_submission", + method: "erpnext.payroll.doctype.employee_tax_exemption_declaration.employee_tax_exemption_declaration.make_proof_submission", frm: frm }); }).addClass("btn-primary"); diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json similarity index 98% rename from erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json rename to erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json index 18fad85c4b..de7c348bb2 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json @@ -107,9 +107,9 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-18 14:56:25.625717", + "modified": "2020-06-22 22:49:43.829892", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Employee Tax Exemption Declaration", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py rename to erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.js b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.js similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.js rename to erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.js diff --git a/erpnext/hr/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 similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py rename to erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py diff --git a/erpnext/hr/doctype/taxable_salary_slab/__init__.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/__init__.py similarity index 100% rename from erpnext/hr/doctype/taxable_salary_slab/__init__.py rename to erpnext/payroll/doctype/employee_tax_exemption_declaration_category/__init__.py diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json new file mode 100644 index 0000000000..8c2f9aa370 --- /dev/null +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "creation": "2018-04-13 16:56:23.333041", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "exemption_sub_category", + "exemption_category", + "max_amount", + "amount" + ], + "fields": [ + { + "fieldname": "exemption_sub_category", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Exemption Sub Category", + "options": "Employee Tax Exemption Sub Category", + "reqd": 1 + }, + { + "fetch_from": "exemption_sub_category.exemption_category", + "fieldname": "exemption_category", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Exemption Category", + "options": "Employee Tax Exemption Category", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "exemption_sub_category.max_amount", + "fieldname": "max_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Maximum Exempted Amount", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Declared Amount", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-22 23:41:03.638739", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Employee Tax Exemption Declaration Category", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.py similarity index 73% rename from erpnext/hr/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.py rename to erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.py index 362677e887..bff747f90d 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration_category/employee_tax_exemption_declaration_category.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class EmployeeTaxExemptionDeclarationCategory(Document): diff --git a/erpnext/hr/notification/retention_bonus/__init__.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/__init__.py similarity index 100% rename from erpnext/hr/notification/retention_bonus/__init__.py rename to erpnext/payroll/doctype/employee_tax_exemption_proof_submission/__init__.py diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js similarity index 90% rename from erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js rename to erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js index 66118c0811..715d7553b0 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js @@ -43,7 +43,7 @@ frappe.ui.form.on('Employee Tax Exemption Proof Submission', { frm.add_custom_button(__('Get Details From Declaration'), function() { erpnext.utils.map_current_doc({ - method: "erpnext.hr.doctype.employee_tax_exemption_declaration.employee_tax_exemption_declaration.make_proof_submission", + method: "erpnext.payroll.doctype.employee_tax_exemption_declaration.employee_tax_exemption_declaration.make_proof_submission", source_doctype: "Employee Tax Exemption Declaration", target: frm, date_field: "creation", diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json similarity index 98% rename from erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json rename to erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json index 8b117a25b5..b62b5aab0b 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json @@ -130,9 +130,9 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-18 14:55:51.420016", + "modified": "2020-06-22 22:53:10.412321", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Employee Tax Exemption Proof Submission", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/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 similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py rename to erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.js b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.js similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.js rename to erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.js 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 new file mode 100644 index 0000000000..cb9ed5f971 --- /dev/null +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import create_exemption_category, create_payroll_period + +class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): + def setup(self): + make_employee("employee@proofsubmission.com") + create_payroll_period() + create_exemption_category() + 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)] + }) + 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)] + }) + 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), + ] + }) + self.assertRaises(frappe.ValidationError, declaration.save) diff --git a/erpnext/hr/print_format/salary_slip_based_on_timesheet/__init__.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/__init__.py similarity index 100% rename from erpnext/hr/print_format/salary_slip_based_on_timesheet/__init__.py rename to erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/__init__.py diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json new file mode 100644 index 0000000000..c1f532050a --- /dev/null +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.json @@ -0,0 +1,66 @@ +{ + "actions": [], + "creation": "2018-04-13 17:19:03.006149", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "exemption_sub_category", + "exemption_category", + "max_amount", + "type_of_proof", + "amount" + ], + "fields": [ + { + "fieldname": "exemption_sub_category", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Exemption Sub Category", + "options": "Employee Tax Exemption Sub Category", + "reqd": 1 + }, + { + "fetch_from": "exemption_sub_category.exemption_category", + "fieldname": "exemption_category", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Exemption Category", + "reqd": 1 + }, + { + "fetch_from": "exemption_sub_category.max_amount", + "fieldname": "max_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Maximum Exemption Amount", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "type_of_proof", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Type of Proof", + "reqd": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Actual Amount" + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-22 23:37:08.265600", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Employee Tax Exemption Proof Submission Detail", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.py similarity index 74% rename from erpnext/hr/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.py rename to erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.py index c5d1a8fe5d..0244ae6646 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission_detail/employee_tax_exemption_proof_submission_detail.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class EmployeeTaxExemptionProofSubmissionDetail(Document): diff --git a/erpnext/hr/print_format/salary_slip_standard/__init__.py b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/__init__.py similarity index 100% rename from erpnext/hr/print_format/salary_slip_standard/__init__.py rename to erpnext/payroll/doctype/employee_tax_exemption_sub_category/__init__.py diff --git a/erpnext/hr/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.js b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.js similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.js rename to erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.js diff --git a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.json b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.json new file mode 100644 index 0000000000..f8c4b8bcb0 --- /dev/null +++ b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.json @@ -0,0 +1,86 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2018-05-09 12:47:26.983095", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "exemption_category", + "max_amount", + "is_active" + ], + "fields": [ + { + "fieldname": "exemption_category", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Tax Exemption Category", + "options": "Employee Tax Exemption Category", + "reqd": 1 + }, + { + "fetch_from": "exemption_category.max_amount", + "fetch_if_empty": 1, + "fieldname": "max_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Max Exemption Amount" + }, + { + "default": "1", + "fieldname": "is_active", + "fieldtype": "Check", + "label": "Is Active" + } + ], + "links": [], + "modified": "2020-06-22 23:18:08.254645", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Employee Tax Exemption Sub Category", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/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 similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py rename to erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py diff --git a/erpnext/hr/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_category.js b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_category.js similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_category.js rename to erpnext/payroll/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_category.js diff --git a/erpnext/hr/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_category.py b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_category.py similarity index 100% rename from erpnext/hr/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_category.py rename to erpnext/payroll/doctype/employee_tax_exemption_sub_category/test_employee_tax_exemption_sub_category.py diff --git a/erpnext/hr/report/bank_remittance/__init__.py b/erpnext/payroll/doctype/income_tax_slab/__init__.py similarity index 100% rename from erpnext/hr/report/bank_remittance/__init__.py rename to erpnext/payroll/doctype/income_tax_slab/__init__.py diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js similarity index 100% rename from erpnext/hr/doctype/income_tax_slab/income_tax_slab.js rename to erpnext/payroll/doctype/income_tax_slab/income_tax_slab.js diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json similarity index 97% rename from erpnext/hr/doctype/income_tax_slab/income_tax_slab.json rename to erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json index f74315f32e..6337d5a6d3 100644 --- a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json @@ -94,9 +94,9 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-29 15:08:21.436120", + "modified": "2020-06-22 20:27:13.425084", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Income Tax Slab", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py similarity index 100% rename from erpnext/hr/doctype/income_tax_slab/income_tax_slab.py rename to erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py diff --git a/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/test_income_tax_slab.py similarity index 100% rename from erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py rename to erpnext/payroll/doctype/income_tax_slab/test_income_tax_slab.py diff --git a/erpnext/hr/report/department_analytics/__init__.py b/erpnext/payroll/doctype/income_tax_slab_other_charges/__init__.py similarity index 100% rename from erpnext/hr/report/department_analytics/__init__.py rename to erpnext/payroll/doctype/income_tax_slab_other_charges/__init__.py diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json b/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json similarity index 95% rename from erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json rename to erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json index b23fb3dc31..7f21204591 100644 --- a/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json +++ b/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json @@ -14,18 +14,6 @@ "max_taxable_income" ], "fields": [ - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "columns": 2, - "fieldname": "min_taxable_income", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Min Taxable Income", - "options": "Company:company:default_currency" - }, { "columns": 4, "fieldname": "description", @@ -34,6 +22,10 @@ "label": "Description", "reqd": 1 }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { "columns": 2, "fieldname": "percent", @@ -47,6 +39,14 @@ "fieldtype": "Section Break", "label": "Conditions" }, + { + "columns": 2, + "fieldname": "min_taxable_income", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Min Taxable Income", + "options": "Company:company:default_currency" + }, { "fieldname": "column_break_7", "fieldtype": "Column Break" @@ -62,9 +62,9 @@ ], "istable": 1, "links": [], - "modified": "2020-04-24 13:27:43.598967", + "modified": "2020-06-22 23:33:17.931912", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Income Tax Slab Other Charges", "owner": "Administrator", "permissions": [], diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py b/erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py similarity index 100% rename from erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py rename to erpnext/payroll/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py diff --git a/erpnext/hr/report/salary_register/__init__.py b/erpnext/payroll/doctype/payroll_employee_detail/__init__.py similarity index 100% rename from erpnext/hr/report/salary_register/__init__.py rename to erpnext/payroll/doctype/payroll_employee_detail/__init__.py diff --git a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json new file mode 100644 index 0000000000..bb68e1814a --- /dev/null +++ b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json @@ -0,0 +1,66 @@ +{ + "actions": [], + "creation": "2017-11-30 06:07:33.477781", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "column_break_3", + "department", + "designation" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "read_only": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Department", + "options": "Department", + "read_only": 1 + }, + { + "fetch_from": "employee.designation", + "fieldname": "designation", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Designation", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-22 23:25:13.779032", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Payroll Employee Detail", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/payroll_employee_detail/payroll_employee_detail.py b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.py similarity index 100% rename from erpnext/hr/doctype/payroll_employee_detail/payroll_employee_detail.py rename to erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.py diff --git a/erpnext/stock/report/ordered_items_to_be_delivered/__init__.py b/erpnext/payroll/doctype/payroll_entry/__init__.py similarity index 100% rename from erpnext/stock/report/ordered_items_to_be_delivered/__init__.py rename to erpnext/payroll/doctype/payroll_entry/__init__.py diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js similarity index 91% rename from erpnext/hr/doctype/payroll_entry/payroll_entry.js rename to erpnext/payroll/doctype/payroll_entry/payroll_entry.js index da25d7574e..8d35a7be47 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -30,6 +30,7 @@ frappe.ui.form.on('Payroll Entry', { ).toggleClass('btn-primary', !(frm.doc.employees || []).length); } if ((frm.doc.employees || []).length) { + frm.page.clear_primary_action(); frm.page.set_primary_action(__('Create Salary Slips'), () => { frm.save('Submit').then(()=>{ frm.page.clear_primary_action(); @@ -49,13 +50,14 @@ frappe.ui.form.on('Payroll Entry', { return frappe.call({ doc: frm.doc, method: 'fill_employee_details', - callback: function(r) { - if (r.docs[0].employees){ - frm.save(); - frm.refresh(); - if(r.docs[0].validate_attendance){ - render_employee_attendance(frm, r.message); - } + }).then(r => { + if (r.docs && r.docs[0].employees){ + frm.employees = r.docs[0].employees; + frm.dirty(); + frm.save(); + frm.refresh(); + if(r.docs[0].validate_attendance){ + render_employee_attendance(frm, r.message); } } }) @@ -84,7 +86,7 @@ frappe.ui.form.on('Payroll Entry', { add_bank_entry_button: function(frm) { frappe.call({ - method: 'erpnext.hr.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries', + method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries', args: { 'name': frm.doc.name }, @@ -170,7 +172,7 @@ frappe.ui.form.on('Payroll Entry', { set_start_end_dates: function (frm) { if (!frm.doc.salary_slip_based_on_timesheet) { frappe.call({ - method: 'erpnext.hr.doctype.payroll_entry.payroll_entry.get_start_end_dates', + method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_start_end_dates', args: { payroll_frequency: frm.doc.payroll_frequency, start_date: frm.doc.posting_date @@ -188,7 +190,7 @@ frappe.ui.form.on('Payroll Entry', { set_end_date: function(frm){ frappe.call({ - method: 'erpnext.hr.doctype.payroll_entry.payroll_entry.get_end_date', + method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', args: { frequency: frm.doc.payroll_frequency, start_date: frm.doc.start_date diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json similarity index 98% rename from erpnext/hr/doctype/payroll_entry/payroll_entry.json rename to erpnext/payroll/doctype/payroll_entry/payroll_entry.json index 9356f3e7b3..31a899699d 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.json +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_copy": 1, "autoname": "HR-PRUN-.YYYY.-.#####", "creation": "2017-10-23 15:22:29.291323", @@ -260,9 +261,10 @@ ], "icon": "fa fa-cog", "is_submittable": 1, - "modified": "2019-09-12 15:46:31.436381", + "links": [], + "modified": "2020-06-22 20:06:06.953904", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Payroll Entry", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py similarity index 99% rename from erpnext/hr/doctype/payroll_entry/payroll_entry.py rename to erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 656de0170d..ad9b6d86c8 100644 --- a/erpnext/hr/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -55,7 +55,7 @@ class PayrollEntry(Document): ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s {condition}""".format(condition=condition), {"company": self.company, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet}) - + if sal_struct: cond += "and t2.salary_structure IN %(sal_struct)s " cond += "and %(from_date)s >= t2.from_date" @@ -154,7 +154,7 @@ class PayrollEntry(Document): submit_salary_slips_for_employees(self, ss_list, publish_progress=False) def email_salary_slip(self, submitted_ss): - if frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee"): + if frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee"): for ss in submitted_ss: ss.email_salary_slip() @@ -170,7 +170,7 @@ class PayrollEntry(Document): def get_salary_components(self, component_type): salary_slips = self.get_sal_slip_list(ss_status = 1, as_dict = True) - if salary_slips: + if salary_slips: salary_components = frappe.db.sql(""" select ssd.salary_component, ssd.amount, ssd.parentfield, ss.payroll_cost_center from `tabSalary Slip` ss, `tabSalary Detail` ssd @@ -197,7 +197,7 @@ class PayrollEntry(Document): return account_details def get_account(self, component_dict = None): - account_dict = {} + account_dict = {} for key, amount in component_dict.items(): account = self.get_salary_component_account(key[0]) account_dict[(account, key[1])] = account_dict.get((account, key[1]), 0) + amount @@ -539,6 +539,7 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr if not_submitted_ss: frappe.msgprint(_("Could not submit some Salary Slips")) +@frappe.whitelist() def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" select name from `tabPayroll Entry` diff --git a/erpnext/hr/doctype/payroll_entry/payroll_entry_dashboard.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py similarity index 100% rename from erpnext/hr/doctype/payroll_entry/payroll_entry_dashboard.py rename to erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py diff --git a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.js similarity index 100% rename from erpnext/hr/doctype/payroll_entry/test_payroll_entry.js rename to erpnext/payroll/doctype/payroll_entry/test_payroll_entry.js diff --git a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py similarity index 96% rename from erpnext/hr/doctype/payroll_entry/test_payroll_entry.py rename to erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 3c318e78a2..b0f225d909 100644 --- a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -7,11 +7,11 @@ import frappe from dateutil.relativedelta import relativedelta from erpnext.accounts.utils import get_fiscal_year, getdate, nowdate from frappe.utils import add_months -from erpnext.hr.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date +from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates, get_end_date from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.salary_slip.test_salary_slip import get_salary_component_account, \ +from erpnext.payroll.doctype.salary_slip.test_salary_slip import get_salary_component_account, \ make_earning_salary_component, make_deduction_salary_component, create_account -from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans @@ -24,7 +24,7 @@ class TestPayrollEntry(unittest.TestCase): make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, company_list=["_Test Company"]) - frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0) + frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) def test_payroll_entry(self): # pylint: disable=no-self-use company = erpnext.get_default_company() diff --git a/erpnext/hr/doctype/payroll_entry/test_set_salary_components.js b/erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js similarity index 100% rename from erpnext/hr/doctype/payroll_entry/test_set_salary_components.js rename to erpnext/payroll/doctype/payroll_entry/test_set_salary_components.js diff --git a/erpnext/stock/report/purchase_order_items_to_be_received/__init__.py b/erpnext/payroll/doctype/payroll_period/__init__.py similarity index 100% rename from erpnext/stock/report/purchase_order_items_to_be_received/__init__.py rename to erpnext/payroll/doctype/payroll_period/__init__.py diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.js b/erpnext/payroll/doctype/payroll_period/payroll_period.js similarity index 100% rename from erpnext/hr/doctype/payroll_period/payroll_period.js rename to erpnext/payroll/doctype/payroll_period/payroll_period.js diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.json b/erpnext/payroll/doctype/payroll_period/payroll_period.json similarity index 95% rename from erpnext/hr/doctype/payroll_period/payroll_period.json rename to erpnext/payroll/doctype/payroll_period/payroll_period.json index c0fa506e7f..0e0948475c 100644 --- a/erpnext/hr/doctype/payroll_period/payroll_period.json +++ b/erpnext/payroll/doctype/payroll_period/payroll_period.json @@ -53,9 +53,9 @@ } ], "links": [], - "modified": "2020-03-18 18:13:23.859980", + "modified": "2020-06-29 17:17:12.689089", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Payroll Period", "owner": "Administrator", "permissions": [ @@ -96,6 +96,7 @@ "write": 1 } ], + "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py similarity index 97% rename from erpnext/hr/doctype/payroll_period/payroll_period.py rename to erpnext/payroll/doctype/payroll_period/payroll_period.py index 6956c38285..d7893d0657 100644 --- a/erpnext/hr/doctype/payroll_period/payroll_period.py +++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py @@ -64,7 +64,7 @@ def get_payroll_period_days(start_date, end_date, employee, company=None): 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("HR Settings", None, "include_holidays_in_total_working_days")): + if not cint(frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days")): holidays = get_holidays_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 diff --git a/erpnext/hr/doctype/payroll_period/payroll_period_dashboard.py b/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py similarity index 100% rename from erpnext/hr/doctype/payroll_period/payroll_period_dashboard.py rename to erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py diff --git a/erpnext/hr/doctype/payroll_period/test_payroll_period.js b/erpnext/payroll/doctype/payroll_period/test_payroll_period.js similarity index 100% rename from erpnext/hr/doctype/payroll_period/test_payroll_period.js rename to erpnext/payroll/doctype/payroll_period/test_payroll_period.js diff --git a/erpnext/hr/doctype/payroll_period/test_payroll_period.py b/erpnext/payroll/doctype/payroll_period/test_payroll_period.py similarity index 100% rename from erpnext/hr/doctype/payroll_period/test_payroll_period.py rename to erpnext/payroll/doctype/payroll_period/test_payroll_period.py diff --git a/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/__init__.py b/erpnext/payroll/doctype/payroll_period_date/__init__.py similarity index 100% rename from erpnext/stock/report/purchase_order_items_to_be_received_or_billed/__init__.py rename to erpnext/payroll/doctype/payroll_period_date/__init__.py diff --git a/erpnext/payroll/doctype/payroll_period_date/payroll_period_date.json b/erpnext/payroll/doctype/payroll_period_date/payroll_period_date.json new file mode 100644 index 0000000000..4a2f383b37 --- /dev/null +++ b/erpnext/payroll/doctype/payroll_period_date/payroll_period_date.json @@ -0,0 +1,39 @@ +{ + "actions": [], + "creation": "2018-04-13 15:17:30.513630", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "start_date", + "end_date" + ], + "fields": [ + { + "fieldname": "start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Start Date", + "reqd": 1 + }, + { + "fieldname": "end_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "End Date", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-22 23:30:15.943356", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Payroll Period Date", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/payroll_period_date/payroll_period_date.py b/erpnext/payroll/doctype/payroll_period_date/payroll_period_date.py similarity index 71% rename from erpnext/hr/doctype/payroll_period_date/payroll_period_date.py rename to erpnext/payroll/doctype/payroll_period_date/payroll_period_date.py index 06ecb495f3..a3ee269d8e 100644 --- a/erpnext/hr/doctype/payroll_period_date/payroll_period_date.py +++ b/erpnext/payroll/doctype/payroll_period_date/payroll_period_date.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class PayrollPeriodDate(Document): diff --git a/erpnext/support/doctype/service_level/__init__.py b/erpnext/payroll/doctype/payroll_settings/__init__.py similarity index 100% rename from erpnext/support/doctype/service_level/__init__.py rename to erpnext/payroll/doctype/payroll_settings/__init__.py diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.js b/erpnext/payroll/doctype/payroll_settings/payroll_settings.js new file mode 100644 index 0000000000..941464dc51 --- /dev/null +++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.js @@ -0,0 +1,19 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Payroll Settings', { + encrypt_salary_slips_in_emails: function(frm) { + let encrypt_state = frm.doc.encrypt_salary_slips_in_emails; + frm.set_df_property('password_policy', 'reqd', encrypt_state); + }, + + validate: function(frm) { + let policy = frm.doc.password_policy; + if (policy) { + if (policy.includes(' ') || policy.includes('--')) { + frappe.msgprint(__("Password policy cannot contain spaces or simultaneous hyphens. The format will be restructured automatically")); + } + frm.set_value('password_policy', policy.split(new RegExp(" |-", 'g')).filter((token) => token).join('-')); + } + }, +}); diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json new file mode 100644 index 0000000000..c47caa1227 --- /dev/null +++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json @@ -0,0 +1,130 @@ +{ + "actions": [], + "creation": "2020-06-04 15:13:33.589685", + "doctype": "DocType", + "document_type": "Other", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "payroll_based_on", + "consider_unmarked_attendance_as", + "max_working_hours_against_timesheet", + "include_holidays_in_total_working_days", + "disable_rounded_total", + "column_break_11", + "daily_wages_fraction_for_half_day", + "email_salary_slip_to_employee", + "encrypt_salary_slips_in_emails", + "password_policy" + ], + "fields": [ + { + "default": "Leave", + "fieldname": "payroll_based_on", + "fieldtype": "Select", + "label": "Calculate Payroll Working Days Based On", + "options": "Leave\nAttendance", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "max_working_hours_against_timesheet", + "fieldtype": "Float", + "label": "Max working hours against Timesheet", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "description": "If checked, Total no. of Working Days will include holidays, and this will reduce the value of Salary Per Day", + "fieldname": "include_holidays_in_total_working_days", + "fieldtype": "Check", + "label": "Include holidays in Total no. of Working Days", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "description": "If checked, hides and disables Rounded Total field in Salary Slips", + "fieldname": "disable_rounded_total", + "fieldtype": "Check", + "label": "Disable Rounded Total", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0.5", + "description": "The fraction of daily wages to be paid for half-day attendance", + "fieldname": "daily_wages_fraction_for_half_day", + "fieldtype": "Float", + "label": "Fraction of Daily Salary for Half Day", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "1", + "description": "Emails salary slip to employee based on preferred email selected in Employee", + "fieldname": "email_salary_slip_to_employee", + "fieldtype": "Check", + "label": "Email Salary Slip to Employee", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.email_salary_slip_to_employee == 1;", + "description": "The salary slip emailed to the employee will be password protected, the password will be generated based on the password policy.", + "fieldname": "encrypt_salary_slips_in_emails", + "fieldtype": "Check", + "label": "Encrypt Salary Slips in Emails", + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval: doc.encrypt_salary_slips_in_emails == 1", + "description": "Example: SAL-{first_name}-{date_of_birth.year}
This will generate a password like SAL-Jane-1972", + "fieldname": "password_policy", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Password Policy", + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval:doc.payroll_based_on == 'Attendance'", + "fieldname": "consider_unmarked_attendance_as", + "fieldtype": "Select", + "label": "Consider Unmarked Attendance As", + "options": "Present\nAbsent", + "show_days": 1, + "show_seconds": 1 + } + ], + "icon": "fa fa-cog", + "issingle": 1, + "links": [], + "modified": "2020-06-22 17:00:58.408030", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Payroll Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "ASC" +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.py b/erpnext/payroll/doctype/payroll_settings/payroll_settings.py new file mode 100644 index 0000000000..5efa41db1f --- /dev/null +++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe.utils import cint +from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe import _ + +class PayrollSettings(Document): + def validate(self): + self.validate_password_policy() + + if not self.daily_wages_fraction_for_half_day: + self.daily_wages_fraction_for_half_day = 0.5 + + def validate_password_policy(self): + if self.email_salary_slip_to_employee and self.encrypt_salary_slips_in_emails: + 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") + make_property_setter("Salary Slip", "rounded_total", "print_hide", self.disable_rounded_total, "Check") diff --git a/erpnext/payroll/doctype/payroll_settings/test_payroll_settings.py b/erpnext/payroll/doctype/payroll_settings/test_payroll_settings.py new file mode 100644 index 0000000000..314866e128 --- /dev/null +++ b/erpnext/payroll/doctype/payroll_settings/test_payroll_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestPayrollSettings(unittest.TestCase): + pass diff --git a/erpnext/payroll/doctype/retention_bonus/__init__.py b/erpnext/payroll/doctype/retention_bonus/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/retention_bonus/retention_bonus.js b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js similarity index 100% rename from erpnext/hr/doctype/retention_bonus/retention_bonus.js rename to erpnext/payroll/doctype/retention_bonus/retention_bonus.js diff --git a/erpnext/hr/doctype/retention_bonus/retention_bonus.json b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json similarity index 90% rename from erpnext/hr/doctype/retention_bonus/retention_bonus.json rename to erpnext/payroll/doctype/retention_bonus/retention_bonus.json index 7781053e13..da884c2f28 100644 --- a/erpnext/hr/doctype/retention_bonus/retention_bonus.json +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "HR-RTB-.YYYY.-.#####", @@ -16,8 +17,7 @@ "column_break_6", "employee_name", "department", - "date_of_joining", - "additional_salary" + "date_of_joining" ], "fields": [ { @@ -83,14 +83,6 @@ "label": "Date of Joining", "read_only": 1 }, - { - "fieldname": "additional_salary", - "fieldtype": "Link", - "label": "Additional Salary", - "no_copy": 1, - "options": "Additional Salary", - "read_only": 1 - }, { "fieldname": "salary_component", "fieldtype": "Link", @@ -100,9 +92,10 @@ } ], "is_submittable": 1, - "modified": "2019-09-03 16:47:24.210422", + "links": [], + "modified": "2020-06-22 22:42:05.251951", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Retention Bonus", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/retention_bonus/retention_bonus.py b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py similarity index 73% rename from erpnext/hr/doctype/retention_bonus/retention_bonus.py rename to erpnext/payroll/doctype/retention_bonus/retention_bonus.py index 48637a350c..b8e56ae42a 100644 --- a/erpnext/hr/doctype/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py @@ -17,13 +17,7 @@ class RetentionBonus(Document): def on_submit(self): company = frappe.db.get_value('Employee', self.employee, 'company') - additional_salary = frappe.db.exists('Additional Salary', { - 'employee': self.employee, - 'salary_component': self.salary_component, - 'payroll_date': self.bonus_payment_date, - 'company': company, - 'docstatus': 1 - }) + additional_salary = self.get_additional_salary() if not additional_salary: additional_salary = frappe.new_doc('Additional Salary') @@ -32,8 +26,11 @@ class RetentionBonus(Document): additional_salary.amount = self.bonus_amount additional_salary.payroll_date = self.bonus_payment_date additional_salary.company = company + additional_salary.overwrite_salary_structure_amount = 0 + additional_salary.ref_doctype = self.doctype + additional_salary.ref_docname = self.name additional_salary.submit() - self.db_set('additional_salary', additional_salary.name) + # self.db_set('additional_salary', additional_salary.name) else: bonus_added = frappe.db.get_value('Additional Salary', additional_salary, 'amount') + self.bonus_amount @@ -41,11 +38,24 @@ class RetentionBonus(Document): 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 if bonus_removed == 0: frappe.get_doc('Additional Salary', self.additional_salary).cancel() else: frappe.db.set_value('Additional Salary', self.additional_salary, 'amount', bonus_removed) - - self.db_set('additional_salary', '') \ No newline at end of file + + # 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 + }) diff --git a/erpnext/hr/doctype/retention_bonus/test_retention_bonus.js b/erpnext/payroll/doctype/retention_bonus/test_retention_bonus.js similarity index 100% rename from erpnext/hr/doctype/retention_bonus/test_retention_bonus.js rename to erpnext/payroll/doctype/retention_bonus/test_retention_bonus.js diff --git a/erpnext/hr/doctype/retention_bonus/test_retention_bonus.py b/erpnext/payroll/doctype/retention_bonus/test_retention_bonus.py similarity index 100% rename from erpnext/hr/doctype/retention_bonus/test_retention_bonus.py rename to erpnext/payroll/doctype/retention_bonus/test_retention_bonus.py diff --git a/erpnext/hr/doctype/salary_component/README.md b/erpnext/payroll/doctype/salary_component/README.md similarity index 100% rename from erpnext/hr/doctype/salary_component/README.md rename to erpnext/payroll/doctype/salary_component/README.md diff --git a/erpnext/payroll/doctype/salary_component/__init__.py b/erpnext/payroll/doctype/salary_component/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/salary_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js similarity index 100% rename from erpnext/hr/doctype/salary_component/salary_component.js rename to erpnext/payroll/doctype/salary_component/salary_component.js diff --git a/erpnext/hr/doctype/salary_component/salary_component.json b/erpnext/payroll/doctype/salary_component/salary_component.json similarity index 95% rename from erpnext/hr/doctype/salary_component/salary_component.json rename to erpnext/payroll/doctype/salary_component/salary_component.json index 97c46c829e..225b048293 100644 --- a/erpnext/hr/doctype/salary_component/salary_component.json +++ b/erpnext/payroll/doctype/salary_component/salary_component.json @@ -16,6 +16,7 @@ "column_break_4", "depends_on_payment_days", "is_tax_applicable", + "is_income_tax_component", "deduct_full_tax_on_selected_payroll_date", "variable_based_on_taxable_salary", "exempted_from_income_tax", @@ -231,13 +232,22 @@ "fieldname": "exempted_from_income_tax", "fieldtype": "Check", "label": "Exempted from Income Tax" + }, + { + "default": "0", + "depends_on": "eval:doc.type == \"Deduction\"", + "fieldname": "is_income_tax_component", + "fieldtype": "Check", + "label": "Is Income Tax Component", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-flag", "links": [], - "modified": "2020-04-28 15:46:45.252945", + "modified": "2020-06-22 15:39:20.826565", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Salary Component", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/salary_component/salary_component.py b/erpnext/payroll/doctype/salary_component/salary_component.py similarity index 100% rename from erpnext/hr/doctype/salary_component/salary_component.py rename to erpnext/payroll/doctype/salary_component/salary_component.py diff --git a/erpnext/hr/doctype/salary_component/test_records.json b/erpnext/payroll/doctype/salary_component/test_records.json similarity index 100% rename from erpnext/hr/doctype/salary_component/test_records.json rename to erpnext/payroll/doctype/salary_component/test_records.json diff --git a/erpnext/hr/doctype/salary_component/test_salary_component.js b/erpnext/payroll/doctype/salary_component/test_salary_component.js similarity index 100% rename from erpnext/hr/doctype/salary_component/test_salary_component.js rename to erpnext/payroll/doctype/salary_component/test_salary_component.js diff --git a/erpnext/hr/doctype/salary_component/test_salary_component.py b/erpnext/payroll/doctype/salary_component/test_salary_component.py similarity index 100% rename from erpnext/hr/doctype/salary_component/test_salary_component.py rename to erpnext/payroll/doctype/salary_component/test_salary_component.py diff --git a/erpnext/payroll/doctype/salary_detail/__init__.py b/erpnext/payroll/doctype/salary_detail/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json similarity index 99% rename from erpnext/hr/doctype/salary_detail/salary_detail.json rename to erpnext/payroll/doctype/salary_detail/salary_detail.json index fe5f83b532..adb54f26c6 100644 --- a/erpnext/hr/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -211,13 +211,13 @@ ], "istable": 1, "links": [], - "modified": "2020-04-04 20:00:16.475295", + "modified": "2020-06-22 23:21:26.300951", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Salary Detail", "owner": "Administrator", "permissions": [], "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_detail/salary_detail.py b/erpnext/payroll/doctype/salary_detail/salary_detail.py similarity index 100% rename from erpnext/hr/doctype/salary_detail/salary_detail.py rename to erpnext/payroll/doctype/salary_detail/salary_detail.py diff --git a/erpnext/hr/doctype/salary_slip/README.md b/erpnext/payroll/doctype/salary_slip/README.md similarity index 100% rename from erpnext/hr/doctype/salary_slip/README.md rename to erpnext/payroll/doctype/salary_slip/README.md diff --git a/erpnext/hr/doctype/salary_slip/__init__.py b/erpnext/payroll/doctype/salary_slip/__init__.py similarity index 100% rename from erpnext/hr/doctype/salary_slip/__init__.py rename to erpnext/payroll/doctype/salary_slip/__init__.py diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js similarity index 93% rename from erpnext/hr/doctype/salary_slip/salary_slip.js rename to erpnext/payroll/doctype/salary_slip/salary_slip.js index 1c4d4e34c5..4b623e57b4 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -56,7 +56,7 @@ frappe.ui.form.on("Salary Slip", { set_end_date: function(frm){ frappe.call({ - method: 'erpnext.hr.doctype.payroll_entry.payroll_entry.get_end_date', + method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', args: { frequency: frm.doc.payroll_frequency, start_date: frm.doc.start_date @@ -123,6 +123,9 @@ frappe.ui.form.on("Salary Slip", { doc: frm.doc, callback: function(r, rt) { frm.refresh(); + if (frm.doc.absent_days){ + frm.fields_dict.absent_days.set_description("Unmarked Days is treated as "+ r.message +". You can can change this in " + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)); + } } }); } diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json similarity index 70% rename from erpnext/hr/doctype/salary_slip/salary_slip.json rename to erpnext/payroll/doctype/salary_slip/salary_slip.json index cfd4d89731..88931c2a4b 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -23,11 +23,13 @@ "salary_slip_based_on_timesheet", "start_date", "end_date", - "column_break_15", "salary_structure", "payroll_frequency", + "column_break_15", "total_working_days", + "unmarked_days", "leave_without_pay", + "absent_days", "payment_days", "hourly_wages", "timesheets", @@ -37,6 +39,7 @@ "section_break_26", "bank_name", "bank_account_no", + "mode_of_payment", "section_break_32", "deduct_tax_for_unclaimed_employee_benefits", "deduct_tax_for_unsubmitted_tax_exemption_proof", @@ -71,7 +74,9 @@ "fieldtype": "Date", "in_list_view": 1, "label": "Posting Date", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "employee", @@ -84,7 +89,9 @@ "oldfieldtype": "Link", "options": "Employee", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "employee.employee_name", @@ -95,7 +102,9 @@ "label": "Employee Name", "oldfieldname": "employee_name", "oldfieldtype": "Data", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "employee.department", @@ -106,7 +115,9 @@ "oldfieldname": "department", "oldfieldtype": "Link", "options": "Department", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.designation", @@ -115,7 +126,9 @@ "fieldtype": "Read Only", "label": "Designation", "oldfieldname": "designation", - "oldfieldtype": "Link" + "oldfieldtype": "Link", + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "employee.branch", @@ -126,12 +139,16 @@ "oldfieldname": "branch", "oldfieldtype": "Link", "options": "Branch", - "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%" }, { @@ -139,21 +156,27 @@ "fieldtype": "Select", "label": "Status", "options": "Draft\nSubmitted\nCancelled", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "journal_entry", "fieldtype": "Link", "label": "Journal Entry", "options": "Journal Entry", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payroll_entry", "fieldtype": "Link", "label": "Payroll Entry", "options": "Payroll Entry", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -163,7 +186,9 @@ "label": "Company", "options": "Company", "remember_last_selected_value": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -172,46 +197,62 @@ "ignore_user_permissions": 1, "label": "Letter Head", "options": "Letter Head", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_10", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "salary_slip_based_on_timesheet", "fieldtype": "Check", "label": "Salary Slip Based on Timesheet", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "start_date", "fieldtype": "Date", - "label": "Start Date" + "label": "Start Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "end_date", "fieldtype": "Date", - "label": "End Date" + "label": "End Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_15", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "salary_structure", "fieldtype": "Link", "label": "Salary Structure", "options": "Salary Structure", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:(!doc.salary_slip_based_on_timesheet)", "fieldname": "payroll_frequency", "fieldtype": "Select", "label": "Payroll Frequency", - "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily" + "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_working_days", @@ -220,14 +261,18 @@ "oldfieldname": "total_days_in_month", "oldfieldtype": "Int", "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "leave_without_pay", "fieldtype": "Float", "label": "Leave Without Pay", "oldfieldname": "leave_without_pay", - "oldfieldtype": "Currency" + "oldfieldtype": "Currency", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_days", @@ -236,38 +281,52 @@ "oldfieldname": "payment_days", "oldfieldtype": "Float", "read_only": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "hourly_wages", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "timesheets", "fieldtype": "Table", "label": "Salary Slip Timesheet", - "options": "Salary Slip Timesheet" + "options": "Salary Slip Timesheet", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_20", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_working_hours", "fieldtype": "Float", "label": "Total Working Hours", - "print_hide_if_no_value": 1 + "print_hide_if_no_value": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate", "options": "Company:company:default_currency", - "print_hide_if_no_value": 1 + "print_hide_if_no_value": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_26", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bank_name", @@ -275,7 +334,9 @@ "label": "Bank Name", "oldfieldname": "bank_name", "oldfieldtype": "Data", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "bank_account_no", @@ -283,35 +344,46 @@ "label": "Bank Account No.", "oldfieldname": "bank_account_no", "oldfieldtype": "Data", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_32", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "deduct_tax_for_unclaimed_employee_benefits", "fieldtype": "Check", - "label": "Deduct Tax For Unclaimed Employee Benefits" + "label": "Deduct Tax For Unclaimed Employee Benefits", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "deduct_tax_for_unsubmitted_tax_exemption_proof", "fieldtype": "Check", - "label": "Deduct Tax For Unsubmitted Tax Exemption Proof" + "label": "Deduct Tax For Unsubmitted Tax Exemption Proof", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "earning_deduction", "fieldtype": "Section Break", "label": "Earning & Deduction", - "oldfieldtype": "Section Break" + "oldfieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "earning", "fieldtype": "Column Break", - "label": "Earning", "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -320,13 +392,16 @@ "label": "Earnings", "oldfieldname": "earning_details", "oldfieldtype": "Table", - "options": "Salary Detail" + "options": "Salary Detail", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "deduction", "fieldtype": "Column Break", - "label": "Deduction", "oldfieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -335,12 +410,16 @@ "label": "Deductions", "oldfieldname": "deduction_details", "oldfieldtype": "Table", - "options": "Salary Detail" + "options": "Salary Detail", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", - "oldfieldtype": "Section Break" + "oldfieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "gross_pay", @@ -349,11 +428,15 @@ "oldfieldname": "gross_pay", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_25", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_deduction", @@ -362,24 +445,32 @@ "oldfieldname": "total_deduction", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "total_loan_repayment", "fieldname": "loan_repayment", "fieldtype": "Section Break", - "label": "Loan repayment" + "label": "Loan repayment", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "loans", "fieldtype": "Table", "label": "Employee Loan", "options": "Salary Slip Loan", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_43", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -387,7 +478,9 @@ "fieldtype": "Currency", "label": "Total Principal Amount", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -395,11 +488,15 @@ "fieldtype": "Currency", "label": "Total Interest Amount", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_45", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -407,12 +504,16 @@ "fieldtype": "Currency", "label": "Total Loan Repayment", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_pay_info", "fieldtype": "Section Break", - "label": "net pay info" + "label": "net pay info", + "show_days": 1, + "show_seconds": 1 }, { "description": "Gross Pay - Total Deduction - Loan Repayment", @@ -422,11 +523,15 @@ "oldfieldname": "net_pay", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_53", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -434,11 +539,15 @@ "fieldtype": "Currency", "label": "Rounded Total", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_55", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "description": "Net Pay (in words) will be visible once you save the Salary Slip.", @@ -447,7 +556,9 @@ "label": "Total in words", "oldfieldname": "net_pay_in_words", "oldfieldtype": "Data", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -459,7 +570,9 @@ "oldfieldtype": "Data", "options": "Salary Slip", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "employee.payroll_cost_center", @@ -468,16 +581,42 @@ "fieldtype": "Link", "label": "Payroll Cost Center", "options": "Cost Center", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "mode_of_payment", + "fieldtype": "Select", + "label": "Mode Of Payment", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "absent_days", + "fieldtype": "Float", + "label": "Absent Days", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "unmarked_days", + "fieldtype": "Float", + "hidden": 1, + "label": "Unmarked days", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-05-05 18:55:26.173629", + "modified": "2020-06-25 14:42:43.921828", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Salary Slip", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py similarity index 91% rename from erpnext/hr/doctype/salary_slip/salary_slip.py rename to erpnext/payroll/doctype/salary_slip/salary_slip.py index 4d5c8437c6..1e2983e421 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -9,14 +9,14 @@ from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, from frappe.model.naming import make_autoname from frappe import msgprint, _ -from erpnext.hr.doctype.payroll_entry.payroll_entry import get_start_end_dates +from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.utilities.transaction_base import TransactionBase from frappe.utils.background_jobs import enqueue -from erpnext.hr.doctype.additional_salary.additional_salary import get_additional_salary_component -from erpnext.hr.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period -from erpnext.hr.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount -from erpnext.hr.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits +from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salary_component +from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period +from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount +from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry class SalarySlip(TransactionBase): @@ -54,8 +54,8 @@ class SalarySlip(TransactionBase): total = self.net_pay if self.is_rounding_total_disabled() else self.rounded_total self.total_in_words = money_in_words(total, company_currency) - if frappe.db.get_single_value("HR Settings", "max_working_hours_against_timesheet"): - max_working_hours = frappe.db.get_single_value("HR Settings", "max_working_hours_against_timesheet") + 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") 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) @@ -67,7 +67,7 @@ class SalarySlip(TransactionBase): self.set_status() self.update_status(self.name) self.make_loan_repayment_entry() - if (frappe.db.get_single_value("HR 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() def on_cancel(self): @@ -93,7 +93,7 @@ class SalarySlip(TransactionBase): frappe.throw(_("To date cannot be before From date")) def is_rounding_total_disabled(self): - return cint(frappe.db.get_single_value("HR Settings", "disable_rounded_total")) + return cint(frappe.db.get_single_value("Payroll Settings", "disable_rounded_total")) def check_existing(self): if not self.salary_slip_based_on_timesheet: @@ -136,6 +136,8 @@ class SalarySlip(TransactionBase): 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() + consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present" + return consider_unmarked_attendance_as def set_time_sheet(self): if self.salary_slip_based_on_timesheet: @@ -175,7 +177,7 @@ class SalarySlip(TransactionBase): .format(self.employee), title=_('Salary Structure Missing')) def pull_sal_struct(self): - from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip + from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip if self.salary_slip_based_on_timesheet: self.salary_structure = self._salary_structure_doc.name @@ -188,8 +190,8 @@ class SalarySlip(TransactionBase): 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): - payroll_based_on = frappe.db.get_value("HR Settings", None, "payroll_based_on") - include_holidays_in_total_working_days = frappe.db.get_single_value("HR Settings", "include_holidays_in_total_working_days") + 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") working_days = date_diff(self.end_date, self.start_date) + 1 if for_preview: @@ -198,17 +200,18 @@ class SalarySlip(TransactionBase): return holidays = self.get_holidays_for_employee(self.start_date, self.end_date) - + if not cint(include_holidays_in_total_working_days): working_days -= len(holidays) if working_days < 0: frappe.throw(_("There are more holidays than working days this month.")) if not payroll_based_on: - frappe.throw(_("Please set Payroll based on in HR settings")) - + frappe.throw(_("Please set Payroll based on in Payroll settings")) + if payroll_based_on == "Attendance": - actual_lwp = self.calculate_lwp_based_on_attendance(holidays) + actual_lwp, absent = self.calculate_lwp_and_absent_days_based_on_attendance(holidays) + self.absent_days = absent else: actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days) @@ -226,9 +229,34 @@ class SalarySlip(TransactionBase): if flt(payment_days) > flt(lwp): self.payment_days = flt(payment_days) - flt(lwp) + + 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" + + if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent": + self.absent_days += unmarked_days #will be treated as absent + self.payment_days -= unmarked_days + if include_holidays_in_total_working_days: + self.absent_days -= len(holidays) + 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 + + return self.total_working_days - marked_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, @@ -277,7 +305,7 @@ class SalarySlip(TransactionBase): lwp = 0 holidays = "','".join(holidays) daily_wages_fraction_for_half_day = \ - flt(frappe.db.get_value("HR Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + 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) @@ -304,15 +332,15 @@ class SalarySlip(TransactionBase): lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 return lwp - - def calculate_lwp_based_on_attendance(self, holidays): + + def calculate_lwp_and_absent_days_based_on_attendance(self, holidays): lwp = 0 + absent = 0 daily_wages_fraction_for_half_day = \ - flt(frappe.db.get_value("HR Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 lwp_leave_types = dict(frappe.get_all("Leave Type", {"is_lwp": 1}, ["name", "include_holiday"], as_list=1)) - attendances = frappe.db.sql(''' SELECT attendance_date, status, leave_type FROM `tabAttendance` @@ -322,7 +350,7 @@ class SalarySlip(TransactionBase): AND docstatus = 1 AND attendance_date between %s and %s ''', 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 lwp_leave_types: continue @@ -332,9 +360,14 @@ class SalarySlip(TransactionBase): (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]): continue - lwp += (1 - daily_wages_fraction_for_half_day) if d.status == "Half Day" else 1 + if d.status == "Half Day": + lwp += (1 - daily_wages_fraction_for_half_day) + elif d.status == "On Leave" and d.leave_type in lwp_leave_types: + lwp += 1 + elif d.status == "Absent": + absent += 1 - return lwp + return lwp, absent def add_earning_for_hourly_wages(self, doc, salary_component, amount): row_exists = False @@ -578,7 +611,7 @@ class SalarySlip(TransactionBase): # 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 without additional earnings with full tax total_taxable_earnings_without_full_tax_addl_components = total_taxable_earnings - current_additional_earnings_with_full_tax @@ -586,7 +619,7 @@ class SalarySlip(TransactionBase): 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 with additional earnings with full tax full_tax_on_additional_earnings = 0.0 if current_additional_earnings_with_full_tax: @@ -622,7 +655,7 @@ class SalarySlip(TransactionBase): select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name - where + where sd.parentfield='earnings' and sd.is_tax_applicable=1 and is_flexible_benefit=0 @@ -841,7 +874,7 @@ class SalarySlip(TransactionBase): if flt(d.max_taxable_income) and flt(d.max_taxable_income) < tax_amount: continue - + tax_amount += tax_amount * flt(d.percent) / 100 return tax_amount @@ -955,13 +988,13 @@ class SalarySlip(TransactionBase): def email_salary_slip(self): receiver = frappe.db.get_value("Employee", self.employee, "prefered_email") - hr_settings = frappe.get_single("HR Settings") + payroll_settings = frappe.get_single("Payroll Settings") message = "Please see attachment" password = None - if hr_settings.encrypt_salary_slips_in_emails: - password = generate_password_for_pdf(hr_settings.password_policy, self.employee) + 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(hr_settings.password_policy) + the password to unlock the PDF is of the format {0}. """.format(payroll_settings.password_policy) if receiver: email_args = { @@ -1004,8 +1037,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"], 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 self.bank_account_no = emp.bank_ac_no diff --git a/erpnext/hr/doctype/salary_slip/salary_slip_list.js b/erpnext/payroll/doctype/salary_slip/salary_slip_list.js similarity index 100% rename from erpnext/hr/doctype/salary_slip/salary_slip_list.js rename to erpnext/payroll/doctype/salary_slip/salary_slip_list.js diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.js b/erpnext/payroll/doctype/salary_slip/test_salary_slip.js similarity index 100% rename from erpnext/hr/doctype/salary_slip/test_salary_slip.js rename to erpnext/payroll/doctype/salary_slip/test_salary_slip.js diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py similarity index 93% rename from erpnext/hr/doctype/salary_slip/test_salary_slip.py rename to erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 3eff738ec8..37cd89a734 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -10,17 +10,17 @@ import random from erpnext.accounts.utils import get_fiscal_year from frappe.utils.make_random import get_random from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day -from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip -from erpnext.hr.doctype.payroll_entry.payroll_entry import get_month_details +from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip +from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration \ +from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration \ import create_payroll_period, create_exemption_category class TestSalarySlip(unittest.TestCase): def setUp(self): setup_test() def tearDown(self): - frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0) + frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") def test_payment_days_based_on_attendance(self): @@ -28,8 +28,8 @@ class TestSalarySlip(unittest.TestCase): no_of_days = self.get_no_of_days() # Payroll based on attendance - frappe.db.set_value("HR Settings", None, "payroll_based_on", "Attendance") - frappe.db.set_value("HR Settings", None, "daily_wages_fraction_for_half_day", 0.75) + 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_for_attendance@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) @@ -47,7 +47,7 @@ class TestSalarySlip(unittest.TestCase): """, (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) # valid 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 @@ -55,7 +55,8 @@ class TestSalarySlip(unittest.TestCase): ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") - self.assertEqual(ss.leave_without_pay, 2.25) + self.assertEqual(ss.leave_without_pay, 1.25) + self.assertEqual(ss.absent_days, 1) days_in_month = no_of_days[0] no_of_holidays = no_of_days[1] @@ -63,17 +64,17 @@ 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)) + 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("HR Settings", None, "payroll_based_on", "Leave") + frappe.db.set_value("Payroll Settings", None, "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("HR Settings", None, "payroll_based_on", "Leave") + frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") emp_id = make_employee("test_for_attendance@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) @@ -104,13 +105,13 @@ class TestSalarySlip(unittest.TestCase): #Gross pay calculation based on attendances gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) - self.assertEqual(ss.gross_pay, gross_pay) + self.assertEqual(flt(ss.gross_pay, 2), flt(gross_pay, 2)) - frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") + frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() - frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 1) + frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) make_employee("test_employee@salary.com") frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) @@ -126,7 +127,7 @@ class TestSalarySlip(unittest.TestCase): def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() - frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0) + frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) make_employee("test_employee@salary.com") frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None) @@ -144,7 +145,7 @@ class TestSalarySlip(unittest.TestCase): def test_payment_days(self): no_of_days = self.get_no_of_days() # Holidays not included in working days - frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 1) + frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) # set joinng date in the same month make_employee("test_employee@salary.com") @@ -200,7 +201,7 @@ class TestSalarySlip(unittest.TestCase): def test_email_salary_slip(self): frappe.db.sql("delete from `tabEmail Queue`") - frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 1) + frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1) make_employee("test_employee@salary.com") ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") @@ -270,7 +271,8 @@ class TestSalarySlip(unittest.TestCase): # as per assigned salary structure 40500 in monthly salary so 236000*5/100/12 frappe.db.sql("""delete from `tabPayroll Period`""") frappe.db.sql("""delete from `tabSalary Component`""") - + frappe.db.sql("""delete from `tabAdditional Salary`""") + payroll_period = create_payroll_period() create_tax_slab(payroll_period, allow_tax_exemption=True) @@ -287,7 +289,7 @@ class TestSalarySlip(unittest.TestCase): for doc in delete_docs: frappe.db.sql("delete from `tab%s` where employee='%s'" % (doc, employee)) - from erpnext.hr.doctype.salary_structure.test_salary_structure import \ + from erpnext.payroll.doctype.salary_structure.test_salary_structure import \ make_salary_structure, create_salary_structure_assignment salary_structure = make_salary_structure("Stucture to test tax", "Monthly", other_details={"max_benefits": 100000}, test_tax=True) @@ -378,7 +380,7 @@ class TestSalarySlip(unittest.TestCase): 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.hr.doctype.salary_structure.test_salary_structure import make_salary_structure + 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" @@ -699,7 +701,7 @@ def setup_test(): make_holiday_list() frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") - frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0) + 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) diff --git a/erpnext/payroll/doctype/salary_slip_timesheet/__init__.py b/erpnext/payroll/doctype/salary_slip_timesheet/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/doctype/salary_slip_timesheet/salary_slip_timesheet.json b/erpnext/payroll/doctype/salary_slip_timesheet/salary_slip_timesheet.json new file mode 100644 index 0000000000..9930c53ec9 --- /dev/null +++ b/erpnext/payroll/doctype/salary_slip_timesheet/salary_slip_timesheet.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "creation": "2016-06-14 19:22:29.811658", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "time_sheet", + "working_hours" + ], + "fields": [ + { + "fieldname": "time_sheet", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Time Sheet", + "options": "Timesheet", + "reqd": 1 + }, + { + "fieldname": "working_hours", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Working Hours", + "no_copy": 1, + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-22 23:27:43.463532", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Salary Slip Timesheet", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_slip_timesheet/salary_slip_timesheet.py b/erpnext/payroll/doctype/salary_slip_timesheet/salary_slip_timesheet.py similarity index 72% rename from erpnext/hr/doctype/salary_slip_timesheet/salary_slip_timesheet.py rename to erpnext/payroll/doctype/salary_slip_timesheet/salary_slip_timesheet.py index 1bbfc53a32..7adb12e83a 100644 --- a/erpnext/hr/doctype/salary_slip_timesheet/salary_slip_timesheet.py +++ b/erpnext/payroll/doctype/salary_slip_timesheet/salary_slip_timesheet.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class SalarySlipTimesheet(Document): diff --git a/erpnext/hr/doctype/salary_structure/README.md b/erpnext/payroll/doctype/salary_structure/README.md similarity index 100% rename from erpnext/hr/doctype/salary_structure/README.md rename to erpnext/payroll/doctype/salary_structure/README.md diff --git a/erpnext/hr/doctype/salary_structure/__init__.py b/erpnext/payroll/doctype/salary_structure/__init__.py similarity index 100% rename from erpnext/hr/doctype/salary_structure/__init__.py rename to erpnext/payroll/doctype/salary_structure/__init__.py diff --git a/erpnext/payroll/doctype/salary_structure/condition_and_formula_help.html b/erpnext/payroll/doctype/salary_structure/condition_and_formula_help.html new file mode 100644 index 0000000000..d07a1ab551 --- /dev/null +++ b/erpnext/payroll/doctype/salary_structure/condition_and_formula_help.html @@ -0,0 +1,47 @@ +

Variables

+ + +

Examples for Conditions and formula

+ \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js similarity index 91% rename from erpnext/hr/doctype/salary_structure/salary_structure.js rename to erpnext/payroll/doctype/salary_structure/salary_structure.js index 7748403513..ad93a2fa4b 100755 --- a/erpnext/hr/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -14,7 +14,32 @@ cur_frm.cscript.onload = function(doc, dt, dn){ frappe.ui.form.on('Salary Structure', { onload: function(frm) { - frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet), + + let help_button = $(` + Condition and Formula Help + `).click(()=>{ + + let d = new frappe.ui.Dialog({ + title: 'Condition and Formula Help', + fields: [ + { + fieldname: 'msg_wrapper', + fieldtype: 'HTML' + } + ] + }); + + let message_html = frappe.render_template("condition_and_formula_help") + + d.fields_dict.msg_wrapper.$wrapper.append(message_html) + + d.show() + }); + let help_button_wrapper = frm.get_field("conditions_and_formula_variable_and_example").$wrapper; + help_button_wrapper.empty(); + help_button_wrapper.append(frm.doc.filters_html).append(help_button) + + frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet) frm.set_query("salary_component", "earnings", function() { return { @@ -114,7 +139,7 @@ frappe.ui.form.on('Salary Structure', { preview_salary_slip: function(frm) { frappe.call({ - method: "erpnext.hr.doctype.salary_structure.salary_structure.get_employees", + method: "erpnext.payroll.doctype.salary_structure.salary_structure.get_employees", args: { salary_structure: frm.doc.name }, @@ -155,7 +180,7 @@ frappe.ui.form.on('Salary Structure', { open_salary_slip: function(frm, employee){ var print_format = frm.doc.salary_slip_based_on_timesheet ? "Salary Slip based on Timesheet" : "Salary Slip Standard"; frappe.call({ - method: "erpnext.hr.doctype.salary_structure.salary_structure.make_salary_slip", + method: "erpnext.payroll.doctype.salary_structure.salary_structure.make_salary_slip", args: { source_name: frm.doc.name, employee: employee, diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json similarity index 71% rename from erpnext/hr/doctype/salary_structure/salary_structure.json rename to erpnext/payroll/doctype/salary_structure/salary_structure.json index 58c4044093..5f94929f0b 100644 --- a/erpnext/hr/doctype/salary_structure/salary_structure.json +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json @@ -22,10 +22,9 @@ "leave_encashment_amount_per_day", "max_benefits", "earning_deduction", - "earning", "earnings", - "deduction", "deductions", + "conditions_and_formula_variable_and_example", "net_pay_detail", "column_break2", "total_earning", @@ -44,17 +43,23 @@ "label": "Company", "options": "Company", "remember_last_selected_value": 1, - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "letter_head", "fieldtype": "Link", "label": "Letter Head", - "options": "Letter Head" + "options": "Letter Head", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -67,7 +72,9 @@ "oldfieldname": "is_active", "oldfieldtype": "Select", "options": "\nYes\nNo", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "Monthly", @@ -75,7 +82,9 @@ "fieldname": "payroll_frequency", "fieldtype": "Select", "label": "Payroll Frequency", - "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily" + "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily", + "show_days": 1, + "show_seconds": 1 }, { "default": "No", @@ -86,46 +95,62 @@ "no_copy": 1, "options": "Yes\nNo", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "time_sheet_earning_detail", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "salary_slip_based_on_timesheet", "fieldtype": "Check", - "label": "Salary Slip Based on Timesheet" + "label": "Salary Slip Based on Timesheet", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_17", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "description": "Salary Component for timesheet based payroll.", "fieldname": "salary_component", "fieldtype": "Link", "label": "Salary Component", - "options": "Salary Component" + "options": "Salary Component", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "leave_encashment_amount_per_day", "fieldtype": "Currency", "label": "Leave Encashment Amount Per Day", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "max_benefits", "fieldtype": "Currency", "label": "Max Benefits (Amount)", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "show_days": 1, + "show_seconds": 1 }, { "description": "Salary breakup based on Earning and Deduction.", @@ -133,15 +158,9 @@ "fieldtype": "Section Break", "oldfieldname": "earning_deduction", "oldfieldtype": "Section Break", - "precision": "2" - }, - { - "fieldname": "earning", - "fieldtype": "Section Break", - "label": "Earning", - "oldfieldname": "col_brk2", - "oldfieldtype": "Column Break", - "width": "50%" + "precision": "2", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "earnings", @@ -149,15 +168,9 @@ "label": "Earnings", "oldfieldname": "earning_details", "oldfieldtype": "Table", - "options": "Salary Detail" - }, - { - "fieldname": "deduction", - "fieldtype": "Section Break", - "label": "Deduction", - "oldfieldname": "col_brk3", - "oldfieldtype": "Column Break", - "width": "50%" + "options": "Salary Detail", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "deductions", @@ -165,16 +178,22 @@ "label": "Deductions", "oldfieldname": "deduction_details", "oldfieldtype": "Table", - "options": "Salary Detail" + "options": "Salary Detail", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_pay_detail", "fieldtype": "Section Break", - "options": "Simple" + "options": "Simple", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break2", "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1, "width": "50%" }, { @@ -185,7 +204,9 @@ "oldfieldname": "total_earning", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_deduction", @@ -195,7 +216,9 @@ "oldfieldname": "total_deduction", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "net_pay", @@ -203,28 +226,38 @@ "hidden": 1, "label": "Net Pay", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "account", "fieldtype": "Section Break", - "label": "Account" + "label": "Account", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", - "options": "Mode of Payment" + "options": "Mode of Payment", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_28", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payment_account", "fieldtype": "Link", "label": "Payment Account", - "options": "Account" + "options": "Account", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "amended_from", @@ -233,16 +266,25 @@ "no_copy": 1, "options": "Salary Structure", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "conditions_and_formula_variable_and_example", + "fieldtype": "HTML", + "label": "Conditions and Formula variable and example", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2019-12-31 16:34:35.087658", + "modified": "2020-06-22 17:07:26.129355", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Salary Structure", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py similarity index 100% rename from erpnext/hr/doctype/salary_structure/salary_structure.py rename to erpnext/payroll/doctype/salary_structure/salary_structure.py diff --git a/erpnext/hr/doctype/salary_structure/salary_structure_dashboard.py b/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py similarity index 100% rename from erpnext/hr/doctype/salary_structure/salary_structure_dashboard.py rename to erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py diff --git a/erpnext/hr/doctype/salary_structure/test_salary_structure.js b/erpnext/payroll/doctype/salary_structure/test_salary_structure.js similarity index 100% rename from erpnext/hr/doctype/salary_structure/test_salary_structure.js rename to erpnext/payroll/doctype/salary_structure/test_salary_structure.js diff --git a/erpnext/hr/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py similarity index 94% rename from erpnext/hr/doctype/salary_structure/test_salary_structure.py rename to erpnext/payroll/doctype/salary_structure/test_salary_structure.py index eb5311e3b8..e04fda8120 100644 --- a/erpnext/hr/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -7,11 +7,11 @@ import unittest import erpnext from frappe.utils.make_random import get_random from frappe.utils import nowdate, add_days, add_years, getdate, add_months -from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip -from erpnext.hr.doctype.salary_slip.test_salary_slip import make_earning_salary_component,\ +from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip +from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_earning_salary_component,\ make_deduction_salary_component, make_employee_salary_slip, create_tax_slab from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import create_payroll_period +from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import create_payroll_period test_dependencies = ["Fiscal Year"] @@ -62,7 +62,7 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(assignment.base * 0.2, ss.deductions[0].amount) def test_amount_totals(self): - frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0) + 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"}) if not sal_slip: sal_slip = make_employee_salary_slip("test_employee_2@salary.com", "Monthly", "Salary Structure Sample") @@ -128,7 +128,7 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do salary_structure_doc.insert() if not dont_submit: salary_structure_doc.submit() - + else: salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure) diff --git a/erpnext/payroll/doctype/salary_structure_assignment/__init__.py b/erpnext/payroll/doctype/salary_structure_assignment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js similarity index 100% rename from erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js rename to erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json similarity index 98% rename from erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json rename to erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json index 0098aa8ec8..c84e034c72 100644 --- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -124,9 +124,9 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-25 18:24:23.617088", + "modified": "2020-06-22 19:58:09.964692", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Salary Structure Assignment", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py similarity index 100% rename from erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.py rename to erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py diff --git a/erpnext/hr/doctype/salary_structure_assignment/test_salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/test_salary_structure_assignment.js similarity index 100% rename from erpnext/hr/doctype/salary_structure_assignment/test_salary_structure_assignment.js rename to erpnext/payroll/doctype/salary_structure_assignment/test_salary_structure_assignment.js diff --git a/erpnext/hr/doctype/salary_structure_assignment/test_salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/test_salary_structure_assignment.py similarity index 100% rename from erpnext/hr/doctype/salary_structure_assignment/test_salary_structure_assignment.py rename to erpnext/payroll/doctype/salary_structure_assignment/test_salary_structure_assignment.py diff --git a/erpnext/payroll/doctype/taxable_salary_slab/__init__.py b/erpnext/payroll/doctype/taxable_salary_slab/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json b/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json new file mode 100644 index 0000000000..94eda4c043 --- /dev/null +++ b/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.json @@ -0,0 +1,66 @@ +{ + "actions": [], + "creation": "2018-04-13 17:42:13.516032", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "from_amount", + "to_amount", + "percent_deduction", + "condition", + "column_break_5", + "html_6" + ], + "fields": [ + { + "default": "0", + "fieldname": "from_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "From Amount", + "reqd": 1 + }, + { + "fieldname": "to_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "To Amount" + }, + { + "default": "0", + "fieldname": "percent_deduction", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "Percent Deduction", + "reqd": 1 + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Condition" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "html_6", + "fieldtype": "HTML", + "options": "

Condition Examples

\n
    \n
  1. Applying tax if employee born between 31-12-1937 and 01-01-1958 (Employees aged 60 to 80)
    \nCondition: date_of_birth>date(1937, 12, 31) and date_of_birth<date(1958, 01, 01)

  2. Applying tax by employee gender
    \nCondition: gender==\"Male\"

  3. \n
  4. Applying tax by Salary Component
    \nCondition: base > 10000
" + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-22 18:16:07.596493", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Taxable Salary Slab", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/taxable_salary_slab/taxable_salary_slab.py b/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.py similarity index 71% rename from erpnext/hr/doctype/taxable_salary_slab/taxable_salary_slab.py rename to erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.py index 23e5ffbe62..49c52557db 100644 --- a/erpnext/hr/doctype/taxable_salary_slab/taxable_salary_slab.py +++ b/erpnext/payroll/doctype/taxable_salary_slab/taxable_salary_slab.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class TaxableSalarySlab(Document): diff --git a/erpnext/payroll/module_onboarding/payroll/payroll.json b/erpnext/payroll/module_onboarding/payroll/payroll.json new file mode 100644 index 0000000000..b5226b2aca --- /dev/null +++ b/erpnext/payroll/module_onboarding/payroll/payroll.json @@ -0,0 +1,50 @@ +{ + "allow_roles": [ + { + "role": "HR Manager" + }, + { + "role": "HR User" + } + ], + "creation": "2020-06-01 12:10:52.560472", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/human-resources/payroll-entry", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-08 14:06:13.994310", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Payroll", + "owner": "Administrator", + "steps": [ + { + "step": "Create Employee" + }, + { + "step": "Create Salary Component" + }, + { + "step": "Create Payroll Period" + }, + { + "step": "Create Income Tax Slab" + }, + { + "step": "Create Salary Structure" + }, + { + "step": "Assign Salary Structure" + }, + { + "step": "Create Salary Slip" + }, + { + "step": "Payroll Settings" + } + ], + "subtitle": "Salary, Compensation, and more.", + "success_message": "The Payroll Module is all set up!", + "title": "Let's Set Up the Payroll Module. " +} \ No newline at end of file diff --git a/erpnext/payroll/notification/as b/erpnext/payroll/notification/as new file mode 100644 index 0000000000..7a39557261 --- /dev/null +++ b/erpnext/payroll/notification/as @@ -0,0 +1 @@ +update from `tabNotification` set module='Payroll' where name = "Retention Bonus" \ No newline at end of file diff --git a/erpnext/payroll/notification/retention_bonus/__init__.py b/erpnext/payroll/notification/retention_bonus/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/notification/retention_bonus/retention_bonus.json b/erpnext/payroll/notification/retention_bonus/retention_bonus.json similarity index 86% rename from erpnext/hr/notification/retention_bonus/retention_bonus.json rename to erpnext/payroll/notification/retention_bonus/retention_bonus.json index cbc8e2d078..50db0338c4 100644 --- a/erpnext/hr/notification/retention_bonus/retention_bonus.json +++ b/erpnext/payroll/notification/retention_bonus/retention_bonus.json @@ -13,10 +13,10 @@ "is_standard": 1, "message": "

{{ _(\"Hello\") }},

\n\n

{{ _(\"Retention Bonus for\") }} {{ doc.employee_name }} {{ _(\"due on\") }} {{ doc.bonus_payment_date }}

", "modified": "2018-05-15 19:00:24.294418", - "modified_by": "ranjith@earthianslive.com", - "module": "HR", + "modified_by": "Administrator", + "module": "Payroll", "name": "Retention Bonus", - "owner": "ranjith@earthianslive.com", + "owner": "Administrator", "recipients": [ { "email_by_role": "HR Manager" diff --git a/erpnext/hr/notification/retention_bonus/retention_bonus.md b/erpnext/payroll/notification/retention_bonus/retention_bonus.md similarity index 100% rename from erpnext/hr/notification/retention_bonus/retention_bonus.md rename to erpnext/payroll/notification/retention_bonus/retention_bonus.md diff --git a/erpnext/hr/notification/retention_bonus/retention_bonus.py b/erpnext/payroll/notification/retention_bonus/retention_bonus.py similarity index 100% rename from erpnext/hr/notification/retention_bonus/retention_bonus.py rename to erpnext/payroll/notification/retention_bonus/retention_bonus.py diff --git a/erpnext/payroll/onboarding_step/assign_salary_structure/assign_salary_structure.json b/erpnext/payroll/onboarding_step/assign_salary_structure/assign_salary_structure.json new file mode 100644 index 0000000000..8a07b10276 --- /dev/null +++ b/erpnext/payroll/onboarding_step/assign_salary_structure/assign_salary_structure.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-06-01 11:58:43.927590", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 11:58:43.927590", + "modified_by": "Administrator", + "name": "Assign Salary Structure", + "owner": "Administrator", + "reference_document": "Salary Structure Assignment", + "show_full_form": 1, + "title": "Assign Salary Structure", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/onboarding_step/create_employee/create_employee.json b/erpnext/payroll/onboarding_step/create_employee/create_employee.json new file mode 100644 index 0000000000..3aa33c6d86 --- /dev/null +++ b/erpnext/payroll/onboarding_step/create_employee/create_employee.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 11:43:25.561152", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 12:26:28.629074", + "modified_by": "Administrator", + "name": "Create Employee", + "owner": "Administrator", + "reference_document": "Employee", + "show_full_form": 0, + "title": "Create Employee", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/payroll/onboarding_step/create_income_tax_slab/create_income_tax_slab.json b/erpnext/payroll/onboarding_step/create_income_tax_slab/create_income_tax_slab.json new file mode 100644 index 0000000000..faada7e411 --- /dev/null +++ b/erpnext/payroll/onboarding_step/create_income_tax_slab/create_income_tax_slab.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-06-01 11:54:54.823796", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 11:54:54.823796", + "modified_by": "Administrator", + "name": "Create Income Tax Slab", + "owner": "Administrator", + "reference_document": "Income Tax Slab", + "show_full_form": 1, + "title": "Create Income Tax Slab", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/onboarding_step/create_payroll_period/create_payroll_period.json b/erpnext/payroll/onboarding_step/create_payroll_period/create_payroll_period.json new file mode 100644 index 0000000000..4bae67546c --- /dev/null +++ b/erpnext/payroll/onboarding_step/create_payroll_period/create_payroll_period.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-06-01 11:53:54.553947", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 11:53:54.553947", + "modified_by": "Administrator", + "name": "Create Payroll Period", + "owner": "Administrator", + "reference_document": "Payroll Period", + "show_full_form": 0, + "title": "Create Payroll Period", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/onboarding_step/create_salary_component/create_salary_component.json b/erpnext/payroll/onboarding_step/create_salary_component/create_salary_component.json new file mode 100644 index 0000000000..002d819618 --- /dev/null +++ b/erpnext/payroll/onboarding_step/create_salary_component/create_salary_component.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-06-01 11:57:04.002073", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 11:57:04.002073", + "modified_by": "Administrator", + "name": "Create Salary Component", + "owner": "Administrator", + "reference_document": "Salary Component", + "show_full_form": 1, + "title": "Create Salary Component", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/onboarding_step/create_salary_slip/create_salary_slip.json b/erpnext/payroll/onboarding_step/create_salary_slip/create_salary_slip.json new file mode 100644 index 0000000000..2aa31f485f --- /dev/null +++ b/erpnext/payroll/onboarding_step/create_salary_slip/create_salary_slip.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-06-01 11:59:29.972393", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 11:59:29.972393", + "modified_by": "Administrator", + "name": "Create Salary Slip", + "owner": "Administrator", + "reference_document": "Salary Slip", + "show_full_form": 1, + "title": "Create Salary Slip", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/onboarding_step/create_salary_structure/create_salary_structure.json b/erpnext/payroll/onboarding_step/create_salary_structure/create_salary_structure.json new file mode 100644 index 0000000000..11d8327259 --- /dev/null +++ b/erpnext/payroll/onboarding_step/create_salary_structure/create_salary_structure.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-06-01 11:57:54.527808", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 11:57:54.527808", + "modified_by": "Administrator", + "name": "Create Salary Structure", + "owner": "Administrator", + "reference_document": "Salary Structure", + "show_full_form": 1, + "title": "Create Salary Structure", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/onboarding_step/payroll_settings/payroll_settings.json b/erpnext/payroll/onboarding_step/payroll_settings/payroll_settings.json new file mode 100644 index 0000000000..946b8c8707 --- /dev/null +++ b/erpnext/payroll/onboarding_step/payroll_settings/payroll_settings.json @@ -0,0 +1,19 @@ +{ + "action": "Go to Page", + "creation": "2020-06-04 16:34:29.664917", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-04 16:34:29.664917", + "modified_by": "Administrator", + "name": "Payroll Settings", + "owner": "Administrator", + "path": "#Form/Payroll Settings", + "show_full_form": 0, + "title": "Payroll Settings", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/print_format/salary_slip_based_on_timesheet/__init__.py b/erpnext/payroll/print_format/salary_slip_based_on_timesheet/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/print_format/salary_slip_based_on_timesheet/salary_slip_based_on_timesheet.json b/erpnext/payroll/print_format/salary_slip_based_on_timesheet/salary_slip_based_on_timesheet.json similarity index 100% rename from erpnext/hr/print_format/salary_slip_based_on_timesheet/salary_slip_based_on_timesheet.json rename to erpnext/payroll/print_format/salary_slip_based_on_timesheet/salary_slip_based_on_timesheet.json diff --git a/erpnext/payroll/print_format/salary_slip_standard/__init__.py b/erpnext/payroll/print_format/salary_slip_standard/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/print_format/salary_slip_standard/salary_slip_standard.json b/erpnext/payroll/print_format/salary_slip_standard/salary_slip_standard.json similarity index 100% rename from erpnext/hr/print_format/salary_slip_standard/salary_slip_standard.json rename to erpnext/payroll/print_format/salary_slip_standard/salary_slip_standard.json diff --git a/erpnext/payroll/report/__init__.py b/erpnext/payroll/report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/report/bank_remittance/__init__.py b/erpnext/payroll/report/bank_remittance/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/report/bank_remittance/bank_remittance.js b/erpnext/payroll/report/bank_remittance/bank_remittance.js similarity index 68% rename from erpnext/hr/report/bank_remittance/bank_remittance.js rename to erpnext/payroll/report/bank_remittance/bank_remittance.js index 1e10f24301..6482ed3451 100644 --- a/erpnext/hr/report/bank_remittance/bank_remittance.js +++ b/erpnext/payroll/report/bank_remittance/bank_remittance.js @@ -5,12 +5,12 @@ frappe.query_reports["Bank Remittance"] = { "filters": [ { - "fieldname":"company", - "label": __("Company"), - "fieldtype": "Link", - "options": "Company", - "default": frappe.defaults.get_user_default("Company"), - "reqd": 1 + fieldname:"company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 }, { fieldname:"from_date", diff --git a/erpnext/hr/report/bank_remittance/bank_remittance.json b/erpnext/payroll/report/bank_remittance/bank_remittance.json similarity index 87% rename from erpnext/hr/report/bank_remittance/bank_remittance.json rename to erpnext/payroll/report/bank_remittance/bank_remittance.json index b8aa4e98d2..2a697b2589 100644 --- a/erpnext/hr/report/bank_remittance/bank_remittance.json +++ b/erpnext/payroll/report/bank_remittance/bank_remittance.json @@ -7,9 +7,9 @@ "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2019-04-26 16:57:52.558895", + "modified": "2020-05-28 00:08:08.097494", "modified_by": "Administrator", - "module": "HR", + "module": "Payroll", "name": "Bank Remittance", "owner": "Administrator", "prepared_report": 0, diff --git a/erpnext/hr/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py similarity index 92% rename from erpnext/hr/report/bank_remittance/bank_remittance.py rename to erpnext/payroll/report/bank_remittance/bank_remittance.py index b2d2c53024..4b052bf5c4 100644 --- a/erpnext/hr/report/bank_remittance/bank_remittance.py +++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py @@ -125,7 +125,10 @@ def get_salary_slips(payroll_entries): # appending company debit accounts for slip in salary_slips: - slip["debit_acc_no"] = payroll_entry_map[slip.payroll_entry]['company_account'] + if slip.payroll_entry: + slip["debit_acc_no"] = payroll_entry_map[slip.payroll_entry]['company_account'] + else: + slip["debit_acc_no"] = None return salary_slips @@ -149,6 +152,9 @@ def set_company_account(payment_accounts, payroll_entries): company_accounts_map[acc.account] = acc for entry in payroll_entries: - entry["company_account"] = company_accounts_map[entry.payment_account]['bank_account_no'] + company_account = '' + if entry.payment_account in company_accounts_map: + 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/__init__.py b/erpnext/payroll/report/income_tax_deductions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.js b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.js new file mode 100644 index 0000000000..4bbb7f6a1b --- /dev/null +++ b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.js @@ -0,0 +1,7 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { + frappe.query_reports["Income Tax Deductions"] = erpnext.salary_slip_deductions_report_filters; +}); \ No newline at end of file diff --git a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.json b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.json new file mode 100644 index 0000000000..cf80398f98 --- /dev/null +++ b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 0, + "creation": "2020-05-30 00:07:56.744372", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-05-30 00:07:56.744372", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Income Tax Deductions", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Salary Slip", + "report_name": "Income Tax Deductions", + "report_type": "Script Report", + "roles": [ + { + "role": "HR User" + }, + { + "role": "HR Manager" + }, + { + "role": "Employee" + } + ] +} \ No newline at end of file diff --git a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py new file mode 100644 index 0000000000..8a79416edb --- /dev/null +++ b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py @@ -0,0 +1,133 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +from frappe import _ + +def execute(filters=None): + data = get_data(filters) + columns = get_columns(filters) if len(data) else [] + + return columns, data + +def get_columns(filters): + columns = [ + { + "label": _("Employee"), + "options": "Employee", + "fieldname": "employee", + "fieldtype": "Link", + "width": 200 + }, + { + "label": _("Employee Name"), + "options": "Employee", + "fieldname": "employee_name", + "fieldtype": "Link", + "width": 160 + }] + + if erpnext.get_region() == "India": + columns.append({ + "label": _("PAN Number"), + "fieldname": "pan_number", + "fieldtype": "Data", + "width": 140 + }) + + 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 + }, + { + "label": _("Gross Pay"), + "fieldname": "gross_pay", + "fieldtype": "Currency", + "options": "currency", + "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"]) ) + + if filters.get("branch"): + conditions.append("sal.branch = '%s' " % (filters["branch"]) ) + + if filters.get("company"): + conditions.append("sal.company = '%s' " % (filters["company"]) ) + + if filters.get("month"): + conditions.append("month(sal.start_date) = '%s' " % (filters["month"])) + + if filters.get("year"): + conditions.append("year(start_date) = '%s' " % (filters["year"])) + + return " and ".join(conditions) + + +def get_data(filters): + + data = [] + + if erpnext.get_region() == "India": + 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 = [comp_type[0] for comp_type in component_types] + + if not len(component_types): + return [] + + 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 + 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) + + for d in entry: + + employee = { + "employee": d.employee, + "employee_name": d.employee_name, + "it_comp": d.salary_component, + "posting_date": d.posting_date, + # "pan_number": employee_pan_dict.get(d.employee), + "it_amount": d.amount, + "gross_pay": d.gross_pay + } + + if erpnext.get_region() == "India": + employee["pan_number"] = employee_pan_dict.get(d.employee) + + data.append(employee) + + return data diff --git a/erpnext/payroll/report/salary_payments_based_on_payment_mode/__init__.py b/erpnext/payroll/report/salary_payments_based_on_payment_mode/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.js b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.js new file mode 100644 index 0000000000..166d982c9c --- /dev/null +++ b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.js @@ -0,0 +1,7 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { + frappe.query_reports["Salary Payments Based On Payment Mode"] = erpnext.salary_slip_deductions_report_filters; +}); \ No newline at end of file diff --git a/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.json b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.json new file mode 100644 index 0000000000..c04cc32b9b --- /dev/null +++ b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 0, + "creation": "2020-06-16 18:43:43.107246", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-06-16 18:43:43.107246", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Salary Payments Based On Payment Mode", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Salary Slip", + "report_name": "Salary Payments Based On Payment Mode", + "report_type": "Script Report", + "roles": [ + { + "role": "HR User" + }, + { + "role": "HR Manager" + }, + { + "role": "Employee" + } + ] +} \ No newline at end of file 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 new file mode 100644 index 0000000000..a0dab63654 --- /dev/null +++ b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py @@ -0,0 +1,179 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +from frappe import _ +from erpnext.regional.report.provident_fund_deductions.provident_fund_deductions import get_conditions + +def execute(filters=None): + mode_of_payments = get_payment_modes() + + if not len(mode_of_payments): + return [], [] + + 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) + + 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 + }] + + for mode in mode_of_payments: + columns.append({ + "label": _(mode), + "fieldname": mode, + "fieldtype": "Currency", + "width": 160 + }) + + columns.append({ + "label": _("Total"), + "fieldname": "total", + "fieldtype": "Currency", + "width": 140 + }) + + return columns + +def get_payment_modes(): + 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 + + for d in entry: + gross_pay += d.gross_pay + if branch_wise_entries.get(d.branch): + branch_wise_entries[d.branch][d.mode_of_payment] = d.net_pay + else: + branch_wise_entries.setdefault(d.branch, {}).setdefault(d.mode_of_payment, d.net_pay) + + return branch_wise_entries, gross_pay + +def get_data(filters, mode_of_payments): + data = [] + + conditions = get_conditions(filters) + + 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) + + branch_wise_entries, gross_pay = prepare_data(entry) + + branches = frappe.db.sql_list(""" + select distinct branch from `tabSalary Slip` sal + where docstatus = 1 %s + """ % (conditions)) + + total_row = {"total": 0, "branch": "Total"} + + for branch in branches: + total = 0 + 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) + + row["total"] = total + data.append(row) + + total_row = get_total_based_on_mode_of_payment(data, mode_of_payments) + total_deductions = gross_pay - total_row.get("total") + + report_summary = [] + + 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") + }) + + currency = erpnext.get_company_currency(filters.company) + 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 + total_row = {"branch": "Total"} + for mode in mode_of_payments: + sum_of_payment = sum([detail[mode] for detail in data if mode in detail.keys()]) + total_row[mode] = sum_of_payment + total += sum_of_payment + + total_row["total"] = total + return total_row + +def get_report_summary(gross_pay, total_deductions, net_pay, currency): + return [ + { + "value": gross_pay, + "label": "Total Gross Pay", + "indicator": "Green", + "datatype": "Currency", + "currency": currency + }, + { + "value": total_deductions, + "label": "Total Deduction", + "datatype": "Currency", + "indicator": "Red", + "currency": currency + }, + { + "value": net_pay, + "label": "Total Net Pay", + "datatype": "Currency", + "indicator": "Blue", + "currency": currency + } + ] + +def get_chart(mode_of_payments, data): + if data: + values = [] + labels = [] + + for mode in mode_of_payments: + values.append(data[mode]) + labels.append([mode]) + + chart = { + "data": { + "labels": labels, + "datasets": [{'name': 'Mode Of Payments', "values": values}] + } + } + chart['type'] = "bar" + return chart diff --git a/erpnext/payroll/report/salary_payments_via_ecs/__init__.py b/erpnext/payroll/report/salary_payments_via_ecs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.js b/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.js new file mode 100644 index 0000000000..e49fc112ff --- /dev/null +++ b/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.js @@ -0,0 +1,16 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { + + let ecs_checklist_filter = erpnext.salary_slip_deductions_report_filters + ecs_checklist_filter['filters'].push({ + fieldname: "type", + label: __("Type"), + fieldtype: "Select", + options:["", "Bank", "Cash", "Cheque"] + }) + + frappe.query_reports["Salary Payments via ECS"] = ecs_checklist_filter +}); diff --git a/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.json b/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.json new file mode 100644 index 0000000000..dd0ac7c4ef --- /dev/null +++ b/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "creation": "2020-06-16 18:35:30.508143", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-06-16 18:38:23.680185", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Salary Payments via ECS", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Salary Slip", + "report_name": "Salary Payments via ECS", + "report_type": "Script Report", + "roles": [ + { + "role": "HR Manager" + }, + { + "role": "HR User" + } + ] +} \ No newline at end of file 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 new file mode 100644 index 0000000000..d09745c37b --- /dev/null +++ b/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py @@ -0,0 +1,149 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +from frappe import _ + +def execute(filters=None): + columns = get_columns(filters) + data = get_data(filters) + + return columns, data + +def get_columns(filters): + columns = [ + { + "label": _("Branch"), + "options": "Branch", + "fieldname": "branch", + "fieldtype": "Link", + "width": 200 + }, + { + "label": _("Employee Name"), + "options": "Employee", + "fieldname": "employee_name", + "fieldtype": "Link", + "width": 160 + }, + { + "label": _("Employee"), + "options":"Employee", + "fieldname": "employee", + "fieldtype": "Link", + "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 + }, + ] + if erpnext.get_region() == "India": + columns += [ + { + "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"]) ) + + if filters.get("branch"): + conditions.append("branch = '%s' " % (filters["branch"]) ) + + if filters.get("company"): + conditions.append("company = '%s' " % (filters["company"]) ) + + if filters.get("month"): + conditions.append("month(start_date) = '%s' " % (filters["month"])) + + if filters.get("year"): + conditions.append("year(start_date) = '%s' " % (filters["year"])) + + return " and ".join(conditions) + +def get_data(filters): + + data = [] + + fields = ["employee", "branch", "bank_name", "bank_ac_no", "salary_mode"] + if erpnext.get_region() == "India": + fields += ["ifsc_code", "micr_code"] + + + 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 + } + ) + + conditions = get_conditions(filters) + + entry = frappe.db.sql(""" select employee, employee_name, gross_pay + from `tabSalary Slip` + where docstatus = 1 %s """ + %(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, + } + + if employee_data_dict.get(d.employee).get("salary_mode") == "Bank": + employee["bank"] = employee_data_dict.get(d.employee).get("bank_name") + employee["account_no"] = employee_data_dict.get(d.employee).get("bank_ac_no") + if erpnext.get_region() == "India": + employee["ifsc"] = employee_data_dict.get(d.employee).get("ifsc_code") + employee["micr"] = employee_data_dict.get(d.employee).get("micr_code") + 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"): + data.append(employee) + elif not filters.get("type"): + data.append(employee) + + return data diff --git a/erpnext/payroll/report/salary_register/__init__.py b/erpnext/payroll/report/salary_register/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/report/salary_register/salary_register.html b/erpnext/payroll/report/salary_register/salary_register.html similarity index 100% rename from erpnext/hr/report/salary_register/salary_register.html rename to erpnext/payroll/report/salary_register/salary_register.html diff --git a/erpnext/hr/report/salary_register/salary_register.js b/erpnext/payroll/report/salary_register/salary_register.js similarity index 100% rename from erpnext/hr/report/salary_register/salary_register.js rename to erpnext/payroll/report/salary_register/salary_register.js diff --git a/erpnext/payroll/report/salary_register/salary_register.json b/erpnext/payroll/report/salary_register/salary_register.json new file mode 100644 index 0000000000..5a70c32593 --- /dev/null +++ b/erpnext/payroll/report/salary_register/salary_register.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 1, + "creation": "2017-01-10 17:36:58.153863", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 2, + "is_standard": "Yes", + "modified": "2020-05-28 00:07:18.576661", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Salary Register", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Salary Slip", + "report_name": "Salary Register", + "report_type": "Script Report", + "roles": [ + { + "role": "HR User" + }, + { + "role": "HR Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/hr/report/salary_register/salary_register.py b/erpnext/payroll/report/salary_register/salary_register.py similarity index 96% rename from erpnext/hr/report/salary_register/salary_register.py rename to erpnext/payroll/report/salary_register/salary_register.py index ea7fc06cff..87010855fd 100644 --- a/erpnext/hr/report/salary_register/salary_register.py +++ b/erpnext/payroll/report/salary_register/salary_register.py @@ -55,8 +55,8 @@ 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:-1", _("Department") + ":Link/Department:-1", - _("Designation") + ":Link/Designation:-1", _("Company") + ":Link/Company:120", _("Start Date") + "::80", - _("End Date") + "::80", _("Leave Without Pay") + ":Float:-1", _("Payment Days") + ":Float:120" + _("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"): []} diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 0993e69e04..f8af30a1c3 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -1,4 +1,5 @@ import frappe +from frappe.utils import cint from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager def get_field_filter_data(): @@ -238,6 +239,7 @@ def get_next_attribute_and_values(item_code, selected_attributes): if exact_match: data = get_product_info_for_website(exact_match[0]) product_info = data.product_info + product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock) if not data.cart_settings.show_price: product_info = None else: diff --git a/erpnext/projects/dashboard_fixtures.py b/erpnext/projects/dashboard_fixtures.py new file mode 100644 index 0000000000..d89ffe9d83 --- /dev/null +++ b/erpnext/projects/dashboard_fixtures.py @@ -0,0 +1,50 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import json +from frappe import _ + +def get_company_for_dashboards(): + company = frappe.defaults.get_defaults().company + if company: + return company + else: + company_list = frappe.get_list("Company") + if company_list: + return company_list[0].name + return None + +def get_data(): + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(), + }) + +def get_dashboards(): + return [{ + "doctype": "Dashboard", + "name": "Project", + "dashboard_name": "Project", + "charts": [ + { "chart": "Project Summary", "width": "Full" } + ] + }] + +def get_charts(): + company = frappe.get_doc("Company", get_company_for_dashboards()) + + return [ + { + 'doctype': 'Dashboard Chart', + 'name': 'Project Summary', + 'chart_name': _('Project Summary'), + 'chart_type': 'Report', + 'report_name': 'Project Summary', + 'is_public': 1, + 'is_custom': 1, + 'filters_json': json.dumps({"company": company.name, "status": "Open"}), + 'type': 'Bar', + 'custom_options': '{"type": "bar", "colors": ["#fc4f51", "#78d6ff", "#7575ff"], "axisOptions": { "shortenYAxisNumbers": 1}, "barOptions": { "stacked": 1 }}', + } + ] \ No newline at end of file diff --git a/erpnext/projects/desk_page/projects/projects.json b/erpnext/projects/desk_page/projects/projects.json index a07cdffcbe..e24cf3081c 100644 --- a/erpnext/projects/desk_page/projects/projects.json +++ b/erpnext/projects/desk_page/projects/projects.json @@ -17,18 +17,23 @@ } ], "category": "Modules", - "charts": [], + "charts": [ + { + "chart_name": "Project Summary", + "label": "Open Projects" + } + ], "creation": "2020-03-02 15:46:04.874669", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Projects", - "modified": "2020-04-01 11:28:51.245756", + "modified": "2020-05-28 13:38:19.934937", "modified_by": "Administrator", "module": "Projects", "name": "Projects", @@ -37,6 +42,7 @@ "pin_to_top": 0, "shortcuts": [ { + "color": "#cef6d1", "format": "{} Assigned", "label": "Task", "link_to": "Task", @@ -44,8 +50,11 @@ "type": "DocType" }, { + "color": "#ffe8cd", + "format": "{} Open", "label": "Project", "link_to": "Project", + "stats_filter": "{\n \"status\": \"Open\"\n}", "type": "DocType" }, { @@ -57,6 +66,11 @@ "label": "Project Billing Summary", "link_to": "Project Billing Summary", "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "Project", + "type": "Dashboard" } ] } \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 5862963496..3570a0f2be 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -18,7 +18,7 @@ frappe.ui.form.on("Project", { }; }, onload: function (frm) { - var so = frm.get_docfield("Project", "sales_order"); + var so = frappe.meta.get_docfield("Project", "sales_order"); so.get_route_options_for_new_doc = function (field) { if (frm.is_new()) return; return { @@ -135,4 +135,4 @@ function open_form(frm, doctype, child_doctype, parentfield) { frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); }); -} +} \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index afdb5b7a01..32ea05b42a 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -238,6 +238,7 @@ def get_list_context(context=None): "row_template": "templates/includes/projects/project_row.html" } +@frappe.whitelist() 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) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 06c62b62d2..0c4f6f1bdf 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -7,7 +7,7 @@ import frappe, unittest test_records = frappe.get_test_records('Project') test_ignore = ["Sales Order"] -from erpnext.projects.doctype.project_template.test_project_template import get_project_template +from erpnext.projects.doctype.project_template.test_project_template import get_project_template, make_project_template from erpnext.projects.doctype.project.project import set_project_status from frappe.utils import getdate @@ -43,4 +43,24 @@ def get_project(name): expected_start_date = '2019-01-01' )).insert() + return project + +def make_project(args): + args = frappe._dict(args) + if args.project_template_name: + template = make_project_template(args.project_template_name) + else: + template = get_project_template() + + project = frappe.get_doc(dict( + doctype = 'Project', + project_name = args.project_name, + status = 'Open', + project_template = template.name, + expected_start_date = args.start_date + )) + + if not frappe.db.exists("Project", args.project_name): + project.insert() + return project \ No newline at end of file diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index efcb2eab68..2c5831a5dc 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -26,4 +26,23 @@ def get_project_template(): ] )).insert() - return frappe.get_doc('Project Template', 'Test Project Template') \ No newline at end of file + return frappe.get_doc('Project Template', 'Test Project Template') + +def make_project_template(project_template_name, project_tasks=[]): + if not frappe.db.exists('Project Template', project_template_name): + frappe.get_doc(dict( + doctype = 'Project Template', + name = project_template_name, + tasks = project_tasks or [ + dict(subject='Task 1', description='Task 1 description', + start=0, duration=3), + dict(subject='Task 2', description='Task 2 description', + start=0, duration=2), + dict(subject='Task 3', description='Task 3 description', + start=2, duration=4), + dict(subject='Task 4', description='Task 4 description', + start=3, duration=2), + ] + )).insert() + + return frappe.get_doc('Project Template', project_template_name) \ No newline at end of file diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js index 5719276669..8c6a9cf8d7 100644 --- a/erpnext/projects/doctype/task/task.js +++ b/erpnext/projects/doctype/task/task.js @@ -3,55 +3,42 @@ frappe.provide("erpnext.projects"); -cur_frm.add_fetch("project", "company", "company"); - frappe.ui.form.on("Task", { - onload: function(frm) { - frm.set_query("task", "depends_on", function() { - var filters = { + setup: function (frm) { + frm.set_query("project", function () { + return { + query: "erpnext.projects.doctype.task.task.get_project" + } + }); + + frm.make_methods = { + 'Timesheet': () => frappe.model.open_mapped_doc({ + method: 'erpnext.projects.doctype.task.task.make_timesheet', + frm: frm + }) + } + }, + + onload: function (frm) { + frm.set_query("task", "depends_on", function () { + let filters = { name: ["!=", frm.doc.name] }; - if(frm.doc.project) filters["project"] = frm.doc.project; + if (frm.doc.project) filters["project"] = frm.doc.project; return { filters: filters }; }) - }, - refresh: function(frm) { - frm.fields_dict['parent_task'].get_query = function () { + frm.set_query("parent_task", function () { + let filters = { + "is_group": 1 + }; + if (frm.doc.project) filters["project"] = frm.doc.project; return { - filters: { - "is_group": 1, - } + filters: filters } - } - - if (!frm.doc.is_group) { - if (!frm.is_new()) { - if (frappe.model.can_read("Timesheet")) { - frm.add_custom_button(__("Timesheet"), () => { - frappe.route_options = { "project": frm.doc.project, "task": frm.doc.name } - frappe.set_route("List", "Timesheet"); - }, __("View"), true); - } - - if (frappe.model.can_read("Expense Claim")) { - frm.add_custom_button(__("Expense Claims"), () => { - frappe.route_options = { "project": frm.doc.project, "task": frm.doc.name }; - frappe.set_route("List", "Expense Claim"); - }, __("View"), true); - } - } - } - }, - - setup: function(frm) { - frm.fields_dict.project.get_query = function() { - return { - query: "erpnext.projects.doctype.task.task.get_project" - } - }; + }); }, is_group: function (frm) { @@ -69,12 +56,8 @@ frappe.ui.form.on("Task", { }) }, - validate: function(frm) { + validate: function (frm) { frm.doc.project && frappe.model.remove_from_locals("Project", frm.doc.project); - }, - + } }); - -cur_frm.add_fetch('task', 'subject', 'subject'); -cur_frm.add_fetch('task', 'project', 'project'); diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index f4b3d3e1ad..27f1a71a52 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -183,7 +183,8 @@ { "fieldname": "progress", "fieldtype": "Percent", - "label": "% Progress" + "label": "% Progress", + "no_copy": 1 }, { "default": "0", @@ -324,6 +325,7 @@ "options": "Department" }, { + "fetch_from": "project.company", "fieldname": "company", "fieldtype": "Link", "label": "Company", @@ -356,6 +358,7 @@ "fieldname": "completed_by", "fieldtype": "Link", "label": "Completed By", + "no_copy": 1, "options": "User" } ], @@ -364,7 +367,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2020-03-18 18:08:44.153211", + "modified": "2020-07-03 12:36:04.960457", "modified_by": "Administrator", "module": "Projects", "name": "Task", diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 1cb2c50cbf..4bdda68b69 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -7,10 +7,11 @@ import json import frappe from frappe import _, throw -from frappe.utils import add_days, cstr, date_diff, get_link_to_form, getdate, today +from frappe.desk.form.assign_to import clear, close_all_assignments +from frappe.model.mapper import get_mapped_doc +from frappe.utils import add_days, cstr, date_diff, get_link_to_form, getdate, today, flt from frappe.utils.nestedset import NestedSet -from frappe.desk.form.assign_to import close_all_assignments, clear -from frappe.utils import date_diff + class CircularReferenceError(frappe.ValidationError): pass class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass @@ -62,10 +63,10 @@ class Task(NestedSet): close_all_assignments(self.doctype, self.name) def validate_progress(self): - if (self.progress or 0) > 100: + if flt(self.progress or 0) > 100: frappe.throw(_("Progress % for a task cannot be more than 100.")) - if self.progress == 100: + if flt(self.progress) == 100: self.status = 'Completed' if self.status == 'Completed': @@ -188,6 +189,7 @@ def check_if_child_exists(name): return child_tasks +@frappe.whitelist() def get_project(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond return frappe.db.sql(""" select name from `tabProject` @@ -219,6 +221,26 @@ def set_tasks_as_overdue(): continue frappe.get_doc("Task", task.name).update_status() + +@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 + }) + + doclist = get_mapped_doc("Task", source_name, { + "Task": { + "doctype": "Timesheet" + } + }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions) + + return doclist + + @frappe.whitelist() def get_children(doctype, parent, task=None, project=None, is_root=False): diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index bd3369447b..47a28fd111 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -64,7 +64,7 @@ class TestTask(unittest.TestCase): def assign(): from frappe.desk.form import assign_to assign_to.add({ - "assign_to": "test@example.com", + "assign_to": ["test@example.com"], "doctype": task.doctype, "name": task.name, "description": "Close this task" diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 32f0428fcd..a5ce44dcf2 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -11,9 +11,9 @@ from frappe.utils import now_datetime, nowdate, add_days, add_months from erpnext.projects.doctype.timesheet.timesheet import OverlapError from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.hr.doctype.salary_structure.test_salary_structure \ +from erpnext.payroll.doctype.salary_structure.test_salary_structure \ import make_salary_structure, create_salary_structure_assignment - +from erpnext.hr.doctype.employee.test_employee import make_employee class TestTimesheet(unittest.TestCase): def setUp(self): @@ -25,8 +25,10 @@ class TestTimesheet(unittest.TestCase): def test_timesheet_billing_amount(self): - make_salary_structure_for_timesheet("_T-Employee-00001") - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1) + emp = make_employee("test_employee_6@salary.com") + + make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate=True, billable=1) self.assertEqual(timesheet.total_hours, 2) self.assertEqual(timesheet.total_billable_hours, 2) @@ -35,8 +37,10 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(timesheet.total_billable_amount, 100) def test_timesheet_billing_amount_not_billable(self): - make_salary_structure_for_timesheet("_T-Employee-00001") - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=0) + emp = make_employee("test_employee_6@salary.com") + + make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate=True, billable=0) self.assertEqual(timesheet.total_hours, 2) self.assertEqual(timesheet.total_billable_hours, 0) @@ -45,8 +49,10 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(timesheet.total_billable_amount, 0) def test_salary_slip_from_timesheet(self): - salary_structure = make_salary_structure_for_timesheet("_T-Employee-00001") - timesheet = make_timesheet("_T-Employee-00001", simulate = True, billable=1) + emp = make_employee("test_employee_6@salary.com") + + salary_structure = make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate = True, billable=1) salary_slip = make_salary_slip(timesheet.name) salary_slip.submit() @@ -65,7 +71,9 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(timesheet.status, 'Submitted') def test_sales_invoice_from_timesheet(self): - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1) + emp = make_employee("test_employee_6@salary.com") + + timesheet = make_timesheet(emp, simulate=True, billable=1) sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer') sales_invoice.due_date = nowdate() sales_invoice.submit() @@ -80,7 +88,9 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(item.rate, 50.00) def test_timesheet_billing_based_on_project(self): - timesheet = make_timesheet("_T-Employee-00001", simulate=True, billable=1, project = '_Test Project', company='_Test Company') + emp = make_employee("test_employee_6@salary.com") + + timesheet = make_timesheet(emp, simulate=True, billable=1, project = '_Test Project', company='_Test Company') sales_invoice = create_sales_invoice(do_not_save=True) sales_invoice.project = '_Test Project' sales_invoice.submit() @@ -90,6 +100,8 @@ class TestTimesheet(unittest.TestCase): 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') initial_setting = settings.ignore_employee_time_overlap settings.ignore_employee_time_overlap = 0 @@ -97,7 +109,7 @@ class TestTimesheet(unittest.TestCase): update_activity_type("_Test Activity Type") timesheet = frappe.new_doc("Timesheet") - timesheet.employee = "_T-Employee-00001" + timesheet.employee = emp timesheet.append( 'time_logs', { @@ -128,50 +140,6 @@ class TestTimesheet(unittest.TestCase): settings.ignore_employee_time_overlap = initial_setting settings.save() - def test_timesheet_std_working_hours(self): - company = frappe.get_doc('Company', "_Test Company") - company.standard_working_hours = 8 - company.save() - - timesheet = frappe.new_doc("Timesheet") - timesheet.employee = "_T-Employee-00001" - timesheet.company = '_Test Company' - timesheet.append( - 'time_logs', - { - "activity_type": "_Test Activity Type", - "from_time": now_datetime(), - "to_time": now_datetime() + datetime.timedelta(days= 4) - } - ) - timesheet.save() - - ts = frappe.get_doc('Timesheet', timesheet.name) - self.assertEqual(ts.total_hours, 32) - ts.submit() - ts.cancel() - - company = frappe.get_doc('Company', "_Test Company") - company.standard_working_hours = 0 - company.save() - - timesheet = frappe.new_doc("Timesheet") - timesheet.employee = "_T-Employee-00001" - timesheet.company = '_Test Company' - timesheet.append( - 'time_logs', - { - "activity_type": "_Test Activity Type", - "from_time": now_datetime(), - "to_time": now_datetime() + datetime.timedelta(days= 4) - } - ) - timesheet.save() - - ts = frappe.get_doc('Timesheet', timesheet.name) - self.assertEqual(ts.total_hours, 96) - ts.submit() - ts.cancel() def make_salary_structure_for_timesheet(employee): salary_structure_name = "Timesheet Salary Structure Test" diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 3eea390ff3..5de2930c1c 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -162,19 +162,11 @@ frappe.ui.form.on("Timesheet Detail", { to_time: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; - var time_diff = (moment(child.to_time).diff(moment(child.from_time),"seconds")) / ( 60 * 60 * 24); - var std_working_hours = 0; if(frm._setting_hours) return; var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600; - std_working_hours = time_diff * frappe.working_hours; - - if (std_working_hours < hours && std_working_hours > 0) { - frappe.model.set_value(cdt, cdn, "hours", std_working_hours); - } else { - frappe.model.set_value(cdt, cdn, "hours", hours); - } + frappe.model.set_value(cdt, cdn, "hours", hours); }, time_logs_add: function(frm) { @@ -236,29 +228,23 @@ var calculate_end_time = function(frm, cdt, cdn) { let d = moment(child.from_time); if(child.hours) { - var time_diff = (moment(child.to_time).diff(moment(child.from_time),"seconds")) / (60 * 60 * 24); - var std_working_hours = 0; - var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600; - - std_working_hours = time_diff * frappe.working_hours; - - if (std_working_hours < hours && std_working_hours > 0) { - frappe.model.set_value(cdt, cdn, "hours", std_working_hours); - frappe.model.set_value(cdt, cdn, "to_time", d.add(hours, "hours").format(frappe.defaultDatetimeFormat)); - } else { - d.add(child.hours, "hours"); - frm._setting_hours = true; - frappe.model.set_value(cdt, cdn, "to_time", - d.format(frappe.defaultDatetimeFormat)).then(() => { - frm._setting_hours = false; - }); - } + d.add(child.hours, "hours"); + frm._setting_hours = true; + frappe.model.set_value(cdt, cdn, "to_time", + d.format(frappe.defaultDatetimeFormat)).then(() => { + frm._setting_hours = false; + }); } }; var update_billing_hours = function(frm, cdt, cdn){ var child = locals[cdt][cdn]; - if(!child.billable) frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0); + if(!child.billable) { + frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0); + } else { + // bill all hours by default + frappe.model.set_value(cdt, cdn, "billing_hours", child.hours); + } }; var update_time_rates = function(frm, cdt, cdn){ diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index e90821689b..7fe22bec4b 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -24,7 +24,6 @@ class Timesheet(Document): self.set_status() self.validate_dates() self.validate_time_logs() - self.calculate_std_hours() self.update_cost() self.calculate_total_amounts() self.calculate_percentage_billed() @@ -91,17 +90,6 @@ class Timesheet(Document): self.start_date = getdate(start_date) self.end_date = getdate(end_date) - def calculate_std_hours(self): - std_working_hours = frappe.get_value("Company", self.company, 'standard_working_hours') - - for time in self.time_logs: - if time.from_time and time.to_time: - if flt(std_working_hours) and date_diff(time.to_time, time.from_time): - time.hours = flt(std_working_hours) * date_diff(time.to_time, time.from_time) - else: - if not time.hours: - time.hours = time_diff_in_hours(time.to_time, time.from_time) - def before_cancel(self): self.set_status() diff --git a/erpnext/projects/report/project_summary/__init__.py b/erpnext/projects/report/project_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/projects/report/project_summary/project_summary.js b/erpnext/projects/report/project_summary/project_summary.js new file mode 100644 index 0000000000..414b7b206a --- /dev/null +++ b/erpnext/projects/report/project_summary/project_summary.js @@ -0,0 +1,42 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Project Summary"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname": "is_active", + "label": __("Is Active"), + "fieldtype": "Select", + "options": "\nYes\nNo", + "default": "Yes", + }, + { + "fieldname": "status", + "label": __("Status"), + "fieldtype": "Select", + "options": "\nOpen\nCompleted\nCancelled", + "default": "Open" + }, + { + "fieldname": "project_type", + "label": __("Project Type"), + "fieldtype": "Link", + "options": "Project Type" + }, + { + "fieldname": "priority", + "label": __("Priority"), + "fieldtype": "Select", + "options": "\nLow\nMedium\nHigh" + } + ] +}; diff --git a/erpnext/projects/report/project_summary/project_summary.json b/erpnext/projects/report/project_summary/project_summary.json new file mode 100644 index 0000000000..0b18b3e278 --- /dev/null +++ b/erpnext/projects/report/project_summary/project_summary.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "creation": "2020-05-04 19:31:54.575765", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-05-04 19:32:53.177213", + "modified_by": "Administrator", + "module": "Projects", + "name": "Project Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Project", + "report_name": "Project Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Projects User" + }, + { + "role": "Projects Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/projects/report/project_summary/project_summary.py b/erpnext/projects/report/project_summary/project_summary.py new file mode 100644 index 0000000000..ea7f1ab2e7 --- /dev/null +++ b/erpnext/projects/report/project_summary/project_summary.py @@ -0,0 +1,155 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns = 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") + + 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"}) + + chart = get_chart_data(data) + report_summary = get_report_summary(data) + + return columns, data, None, chart, report_summary + +def get_columns(): + return [ + { + "fieldname": "name", + "label": _("Project"), + "fieldtype": "Link", + "options": "Project", + "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 + }, + { + "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 + }, + { + "fieldname": "expected_start_date", + "label": _("Start Date"), + "fieldtype": "Date", + "width": 120 + }, + { + "fieldname": "expected_end_date", + "label": _("End Date"), + "fieldtype": "Date", + "width": 120 + }, + ] + +def get_chart_data(data): + labels = [] + total = [] + completed = [] + overdue = [] + + for project in data: + labels.append(project.name) + total.append(project.total_tasks) + completed.append(project.completed_tasks) + overdue.append(project.overdue_tasks) + + return { + "data": { + '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 + } + } + +def get_report_summary(data): + if not data: + return None + + avg_completion = sum([project.percent_complete for project in data]) / len(data) + total = sum([project.total_tasks for project in data]) + total_overdue = sum([project.overdue_tasks for project in data]) + completed = sum([project.completed_tasks for project in data]) + + return [ + { + "value": avg_completion, + "indicator": "Green" if avg_completion > 50 else "Red", + "label": "Average Completion", + "datatype": "Percent", + }, + { + "value": total, + "indicator": "Blue", + "label": "Total Tasks", + "datatype": "Int", + }, + { + "value": completed, + "indicator": "Green", + "label": "Completed Tasks", + "datatype": "Int", + }, + { + "value": total_overdue, + "indicator": "Green" if total_overdue == 0 else "Red", + "label": "Overdue Tasks", + "datatype": "Int", + } + ] diff --git a/erpnext/public/build.json b/erpnext/public/build.json index e94d1ffe5c..2695502269 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -23,8 +23,6 @@ "public/js/queries.js", "public/js/sms_manager.js", "public/js/utils/party.js", - "public/js/templates/address_list.html", - "public/js/templates/contact_list.html", "public/js/controllers/stock_controller.js", "public/js/payment/payments.js", "public/js/controllers/taxes_and_totals.js", diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 9c56189476..a4cc68b3e2 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -73,6 +73,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ me.frm.set_query('contact_person', erpnext.queries.contact_query); me.frm.set_query('supplier_address', erpnext.queries.address_query); + me.frm.set_query('billing_address', erpnext.queries.company_address_query); + if(this.frm.fields_dict.supplier) { this.frm.set_query("supplier", function() { return{ query: "erpnext.controllers.queries.supplier_query" }}); @@ -283,6 +285,11 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ "shipping_address_display", true); }, + billing_address: function() { + erpnext.utils.get_address_display(this.frm, "billing_address", + "billing_address_display", true); + }, + tc_name: function() { this.get_terms(); }, diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js index 2ce49e766b..87b21b78de 100644 --- a/erpnext/public/js/controllers/stock_controller.js +++ b/erpnext/public/js/controllers/stock_controller.js @@ -55,8 +55,9 @@ erpnext.stock.StockController = frappe.ui.form.Controller.extend({ frappe.route_options = { voucher_no: me.frm.doc.name, from_date: me.frm.doc.posting_date, - to_date: me.frm.doc.posting_date, - company: me.frm.doc.company + to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), + company: me.frm.doc.company, + show_cancelled_entries: me.frm.doc.docstatus === 2 }; frappe.set_route("query-report", "Stock Ledger"); }, __("View")); @@ -71,9 +72,10 @@ erpnext.stock.StockController = frappe.ui.form.Controller.extend({ frappe.route_options = { voucher_no: me.frm.doc.name, from_date: me.frm.doc.posting_date, - to_date: me.frm.doc.posting_date, + to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), company: me.frm.doc.company, - group_by: "Group by Voucher (Consolidated)" + group_by: "Group by Voucher (Consolidated)", + show_cancelled_entries: me.frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); }, __("View")); diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index dbe48ec654..b72ceb2113 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -335,6 +335,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ var tax_rate = this._get_tax_rate(tax, item_tax_map); var current_tax_amount = 0.0; + // To set row_id by default as previous row. + if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) { + if (tax.idx === 1) { + frappe.throw( + __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row")); + } + if (!tax.row_id) { + tax.row_id = tax.idx - 1; + } + } if(tax.charge_type == "Actual") { // distribute the tax amount proportionally to each item row var actual = flt(tax.tax_amount, precision("tax_amount", tax)); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 28c2102aef..3c56a636bd 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -159,6 +159,26 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }; }); } + if (this.frm.fields_dict["items"].grid.get_field("cost_center")) { + this.frm.set_query("cost_center", "items", function(doc) { + return { + filters: { + "company": doc.company, + "is_group": 0 + } + }; + }); + } + + if (this.frm.fields_dict["items"].grid.get_field("expense_account")) { + this.frm.set_query("expense_account", "items", function(doc) { + return { + filters: { + "company": doc.company + } + }; + }); + } if(frappe.meta.get_docfield(this.frm.doc.doctype, "pricing_rules")) { this.frm.set_indicator_formatter('pricing_rule', function(doc) { @@ -552,7 +572,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if (show_batch_dialog) return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) .then((r) => { - if(r.message.has_batch_no || r.message.has_serial_no) { + if (r.message && + (r.message.has_batch_no || r.message.has_serial_no)) { frappe.flags.hide_serial_batch_dialog = false; } }); @@ -917,7 +938,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ shipping_rule: function() { var me = this; - if(this.frm.doc.shipping_rule && this.frm.doc.shipping_address) { + if(this.frm.doc.shipping_rule) { return this.frm.call({ doc: this.frm.doc, method: "apply_shipping_rule", @@ -1652,8 +1673,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(!r.exc) { $.each(me.frm.doc.items || [], function(i, item) { if(item.item_code && r.message.hasOwnProperty(item.item_code)) { - item.item_tax_template = r.message[item.item_code].item_tax_template; - item.item_tax_rate = r.message[item.item_code].item_tax_rate; + if (!item.item_tax_template) { + item.item_tax_template = r.message[item.item_code].item_tax_template; + item.item_tax_rate = r.message[item.item_code].item_tax_rate; + } me.add_taxes_from_item_tax_template(item.item_tax_rate); } else { item.item_tax_template = ""; @@ -1709,7 +1732,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, set_gross_profit: function(item) { - if (this.frm.doc.doctype == "Sales Order" && item.valuation_rate) { + if (["Sales Order", "Quotation"].includes(this.frm.doc.doctype) && item.valuation_rate) { var rate = flt(item.rate) * flt(this.frm.doc.conversion_rate || 1); item.gross_profit = flt(((rate - item.valuation_rate) * item.stock_qty), precision("amount", item)); } @@ -1812,7 +1835,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if (doc.tax_category) filters['tax_category'] = doc.tax_category; - + if (doc.company) + filters['company'] = doc.company; return { query: "erpnext.controllers.queries.get_tax_template", filters: filters diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 296c6280d8..d89d4712e6 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -47,6 +47,16 @@ erpnext.financial_statements = { // dropdown for links to other financial statements erpnext.financial_statements.filters = get_filters() + let fiscal_year = frappe.defaults.get_user_default("fiscal_year") + + frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); + frappe.query_report.set_filter_value({ + period_start_date: fy.year_start_date, + period_end_date: fy.year_end_date + }); + }); + report.page.add_inner_button(__("Balance Sheet"), function() { var filters = report.get_values(); frappe.set_route('query-report', 'Balance Sheet', {company: filters.company}); @@ -62,7 +72,7 @@ erpnext.financial_statements = { } }; -function get_filters(){ +function get_filters() { let filters = [ { "fieldname":"company", @@ -99,7 +109,6 @@ function get_filters(){ "fieldname":"period_start_date", "label": __("Start Date"), "fieldtype": "Date", - "default": frappe.datetime.nowdate(), "hidden": 1, "reqd": 1 }, @@ -107,7 +116,6 @@ function get_filters(){ "fieldname":"period_end_date", "label": __("End Date"), "fieldtype": "Date", - "default": frappe.datetime.add_months(frappe.datetime.nowdate(), 12), "hidden": 1, "reqd": 1 }, @@ -162,15 +170,6 @@ function get_filters(){ } ] - erpnext.dimension_filters.forEach((dimension) => { - filters.push({ - "fieldname": dimension["fieldname"], - "label": __(dimension["label"]), - "fieldtype": "Link", - "options": dimension["document_type"] - }); - }); - return filters; } diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js index 17b726ee18..66ff46405d 100644 --- a/erpnext/public/js/help_links.js +++ b/erpnext/public/js/help_links.js @@ -450,7 +450,7 @@ frappe.help.help_links['Form/Opportunity'] = [ ] frappe.help.help_links['Form/Address'] = [ - { label: 'Address', url: docsUrl + 'user/manual/en/CRM/contact' }, + { label: 'Address', url: docsUrl + 'user/manual/en/CRM/address' }, ] frappe.help.help_links['Form/Contact'] = [ diff --git a/erpnext/public/js/purchase_trends_filters.js b/erpnext/public/js/purchase_trends_filters.js index cd767f5d16..c786a8674e 100644 --- a/erpnext/public/js/purchase_trends_filters.js +++ b/erpnext/public/js/purchase_trends_filters.js @@ -51,7 +51,10 @@ erpnext.get_purchase_trends_filters = function() { { "value": "Supplier Group", "label": __("Supplier Group") }, { "value": "Project", "label": __("Project") } ], - "default": "Item" + "default": "Item", + "dashboard_config": { + "read_only": 1 + } }, { "fieldname":"group_by", diff --git a/erpnext/public/js/salary_slip_deductions_report_filters.js b/erpnext/public/js/salary_slip_deductions_report_filters.js new file mode 100644 index 0000000000..2b30e65075 --- /dev/null +++ b/erpnext/public/js/salary_slip_deductions_report_filters.js @@ -0,0 +1,66 @@ +frappe.provide("erpnext.salary_slip_deductions_report_filters"); + +erpnext.salary_slip_deductions_report_filters = { + "filters": [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd:1, + default: frappe.defaults.get_user_default("Company"), + }, + { + fieldname: "month", + label: __("Month"), + fieldtype: "Select", + reqd: 1 , + options: [ + { "value": 1, "label": __("Jan") }, + { "value": 2, "label": __("Feb") }, + { "value": 3, "label": __("Mar") }, + { "value": 4, "label": __("Apr") }, + { "value": 5, "label": __("May") }, + { "value": 6, "label": __("June") }, + { "value": 7, "label": __("July") }, + { "value": 8, "label": __("Aug") }, + { "value": 9, "label": __("Sep") }, + { "value": 10, "label": __("Oct") }, + { "value": 11, "label": __("Nov") }, + { "value": 12, "label": __("Dec") }, + ], + default: frappe.datetime.str_to_obj(frappe.datetime.get_today()).getMonth() + 1 + }, + { + fieldname:"year", + label: __("Year"), + fieldtype: "Select", + reqd: 1 + }, + { + fieldname: "department", + label: __("Department"), + fieldtype: "Link", + options: "Department", + }, + { + fieldname: "branch", + label: __("Barnch"), + fieldtype: "Link", + options: "Branch", + } + ], + + "onload": function() { + return frappe.call({ + method: "erpnext.regional.report.provident_fund_deductions.provident_fund_deductions.get_years", + callback: function(r) { + var year_filter = frappe.query_report.get_filter('year'); + year_filter.df.options = r.message; + year_filter.df.default = r.message.split("\n")[0]; + year_filter.refresh(); + year_filter.set_input(year_filter.df.default); + } + }); + } +} \ No newline at end of file diff --git a/erpnext/public/js/sales_trends_filters.js b/erpnext/public/js/sales_trends_filters.js index b272fdd5fb..b9c4dca913 100644 --- a/erpnext/public/js/sales_trends_filters.js +++ b/erpnext/public/js/sales_trends_filters.js @@ -27,7 +27,10 @@ erpnext.get_sales_trends_filters = function() { { "value": "Territory", "label": __("Territory") }, { "value": "Project", "label": __("Project") } ], - "default": "Item" + "default": "Item", + "dashboard_config": { + "read_only": 1, + } }, { "fieldname":"group_by", diff --git a/erpnext/public/js/templates/address_list.html b/erpnext/public/js/templates/address_list.html deleted file mode 100644 index 0f967b67a0..0000000000 --- a/erpnext/public/js/templates/address_list.html +++ /dev/null @@ -1,22 +0,0 @@ -
-{% for(var i=0, l=addr_list.length; i -

- {%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %} - ({%= __(addr_list[i].address_type) %}){% } %} - {% if(addr_list[i].is_primary_address) { %} - ({%= __("Primary") %}){% } %} - {% if(addr_list[i].is_shipping_address) { %} - ({%= __("Shipping") %}){% } %} - - - {%= __("Edit") %} -

-

{%= addr_list[i].display %}

- -{% } %} -{% if(!addr_list.length) { %} -

{%= __("No address added yet.") %}

-{% } %} -

\ No newline at end of file diff --git a/erpnext/public/js/templates/contact_list.html b/erpnext/public/js/templates/contact_list.html deleted file mode 100644 index 7e6969163b..0000000000 --- a/erpnext/public/js/templates/contact_list.html +++ /dev/null @@ -1,54 +0,0 @@ -
-{% for(var i=0, l=contact_list.length; i -

- {%= contact_list[i].first_name %} {%= contact_list[i].last_name %} - {% if(contact_list[i].is_primary_contact) { %} - ({%= __("Primary") %}) - {% } %} - {% if(contact_list[i].designation){ %} - – {%= contact_list[i].designation %} - {% } %} - - {%= __("Edit") %} -

- {% if (contact_list[i].phones || contact_list[i].email_ids) { %} -

- {% if(contact_list[i].phone) { %} - {%= __("Phone") %}: {%= contact_list[i].phone %} ({%= __("Primary") %})
- {% endif %} - {% if(contact_list[i].mobile_no) { %} - {%= __("Mobile No") %}: {%= contact_list[i].mobile_no %} ({%= __("Primary") %})
- {% endif %} - {% if(contact_list[i].phone_nos) { %} - {% for(var j=0, k=contact_list[i].phone_nos.length; j - {% } %} - {% endif %} -

-

- {% if(contact_list[i].email_id) { %} - {%= __("Email") %}: {%= contact_list[i].email_id %} ({%= __("Primary") %})
- {% endif %} - {% if(contact_list[i].email_ids) { %} - {% for(var j=0, k=contact_list[i].email_ids.length; j - {% } %} - {% endif %} -

- {% endif %} -

- {% if (contact_list[i].address) { %} - {%= __("Address") %}: {%= contact_list[i].address %}
- {% endif %} -

- -{% } %} -{% if(!contact_list.length) { %} -

{%= __("No contacts added yet.") %}

-{% } %} -

-

\ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 58969f2a9f..bcab0d84c6 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -191,6 +191,23 @@ $.extend(erpnext.utils, { }) }, + add_dimensions: function(report_name, index) { + let filters = frappe.query_reports[report_name].filters; + + erpnext.dimension_filters.forEach((dimension) => { + let found = filters.some(el => el.fieldname === dimension['fieldname']); + + if (!found) { + filters.splice(index, 0 ,{ + "fieldname": dimension["fieldname"], + "label": __(dimension["label"]), + "fieldtype": "Link", + "options": dimension["document_type"] + }); + } + }); + }, + make_subscription: function(doctype, docname) { frappe.call({ method: "frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat", @@ -470,7 +487,14 @@ erpnext.utils.update_child_items = function(opts) { fieldtype: 'Date', fieldname: frm.doc.doctype == 'Sales Order' ? "delivery_date" : "schedule_date", in_list_view: 1, - label: frm.doc.doctype == 'Sales Order' ? __("Delivery Date") : __("Reqd by date") + label: frm.doc.doctype == 'Sales Order' ? __("Delivery Date") : __("Reqd by date"), + reqd: 1 + }) + fields.splice(3, 0, { + fieldtype: 'Float', + fieldname: "conversion_factor", + in_list_view: 1, + label: __("Conversion Factor") }) } @@ -519,6 +543,7 @@ erpnext.utils.update_child_items = function(opts) { "item_code": d.item_code, "delivery_date": d.delivery_date, "schedule_date": d.schedule_date, + "conversion_factor": d.conversion_factor, "qty": d.qty, "rate": d.rate, }); diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 99c1b8ae8f..065326744c 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -4,7 +4,7 @@ frappe.provide("erpnext.utils"); erpnext.utils.get_party_details = function(frm, method, args, callback) { - if(!method) { + if (!method) { method = "erpnext.accounts.party.get_party_details"; } @@ -22,12 +22,12 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { } } - if(!args) { - if((frm.doctype != "Purchase Order" && frm.doc.customer) + if (!args) { + if ((frm.doctype != "Purchase Order" && frm.doc.customer) || (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) { let party_type = "Customer"; - if(frm.doc.quotation_to && frm.doc.quotation_to === "Lead") { + if (frm.doc.quotation_to && frm.doc.quotation_to === "Lead") { party_type = "Lead"; } @@ -36,7 +36,7 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { party_type: party_type, price_list: frm.doc.selling_price_list }; - } else if(frm.doc.supplier) { + } else if (frm.doc.supplier) { args = { party: frm.doc.supplier, party_type: "Supplier", @@ -78,13 +78,17 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; } } - if(!args || !args.party) return; + if (!args || !args.party) return; - if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date", + if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { + if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", args.posting_date, args.party_type=="Customer" ? "customer": "supplier")) return; } + if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, args.party_type=="Customer" ? "customer": "supplier")) { + return; + } + args.currency = frm.doc.currency; args.company = frm.doc.company; args.doctype = frm.doc.doctype; @@ -92,14 +96,14 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { method: method, args: args, callback: function(r) { - if(r.message) { + if (r.message) { frm.supplier_tds = r.message.supplier_tds; frm.updating_party_details = true; frappe.run_serially([ () => frm.set_value(r.message), () => { frm.updating_party_details = false; - if(callback) callback(); + if (callback) callback(); frm.refresh(); erpnext.utils.add_item(frm); } @@ -110,9 +114,9 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { } erpnext.utils.add_item = function(frm) { - if(frm.is_new()) { + if (frm.is_new()) { var prev_route = frappe.get_prev_route(); - if(prev_route[1]==='Item' && !(frm.doc.items && frm.doc.items.length)) { + if (prev_route[1]==='Item' && !(frm.doc.items && frm.doc.items.length)) { // add row var item = frm.add_child('items'); frm.refresh_field('items'); @@ -124,23 +128,23 @@ erpnext.utils.add_item = function(frm) { } erpnext.utils.get_address_display = function(frm, address_field, display_field, is_your_company_address) { - if(frm.updating_party_details) return; + if (frm.updating_party_details) return; - if(!address_field) { - if(frm.doctype != "Purchase Order" && frm.doc.customer) { + if (!address_field) { + if (frm.doctype != "Purchase Order" && frm.doc.customer) { address_field = "customer_address"; - } else if(frm.doc.supplier) { + } else if (frm.doc.supplier) { address_field = "supplier_address"; } else return; } - if(!display_field) display_field = "address_display"; - if(frm.doc[address_field]) { + if (!display_field) display_field = "address_display"; + if (frm.doc[address_field]) { frappe.call({ method: "frappe.contacts.doctype.address.address.get_address_display", args: {"address_dict": frm.doc[address_field] }, callback: function(r) { - if(r.message) { + if (r.message) { frm.set_value(display_field, r.message) } } @@ -151,15 +155,15 @@ erpnext.utils.get_address_display = function(frm, address_field, display_field, }; erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billing_address_field, shipping_address_field) { - if(frm.updating_party_details) return; + if (frm.updating_party_details) return; - if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if(!erpnext.utils.validate_mandatory(frm, "Lead/Customer/Supplier", + if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { + if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier", frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) { return; } - if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date", + if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) { return; } @@ -175,8 +179,8 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi "shipping_address": frm.doc[shipping_address_field] }, callback: function(r) { - if(!r.exc){ - if(frm.doc.tax_category != r.message) { + if (!r.exc){ + if (frm.doc.tax_category != r.message) { frm.set_value("tax_category", r.message); } else { erpnext.utils.set_taxes(frm, triggered_from_field); @@ -187,13 +191,17 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi }; erpnext.utils.set_taxes = function(frm, triggered_from_field) { - if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if(!erpnext.utils.validate_mandatory(frm, "Lead/Customer/Supplier", + if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { + if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, triggered_from_field)) { + return; + } + + if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier", frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) { return; } - if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date", + if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) { return; } @@ -230,7 +238,7 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { "shipping_address": frm.doc.shipping_address_name }, callback: function(r) { - if(r.message){ + if (r.message){ frm.set_value("taxes_and_charges", r.message) } } @@ -238,14 +246,14 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { }; erpnext.utils.get_contact_details = function(frm) { - if(frm.updating_party_details) return; + if (frm.updating_party_details) return; - if(frm.doc["contact_person"]) { + if (frm.doc["contact_person"]) { frappe.call({ method: "frappe.contacts.doctype.contact.contact.get_contact_details", args: {contact: frm.doc.contact_person }, callback: function(r) { - if(r.message) + if (r.message) frm.set_value(r.message); } }) @@ -253,10 +261,10 @@ erpnext.utils.get_contact_details = function(frm) { } erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { - if(!value) { + if (!value) { frm.doc[trigger_on] = ""; refresh_field(trigger_on); - frappe.msgprint(__("Please enter {0} first", [label])); + frappe.throw({message:__("Please enter {0} first", [label]), title:__("Mandatory")}); return false; } return true; @@ -271,12 +279,12 @@ erpnext.utils.get_shipping_address = function(frm, callback){ address: frm.doc.shipping_address }, callback: function(r){ - if(r.message){ + if (r.message){ frm.set_value("shipping_address", r.message[0]) //Address title or name frm.set_value("shipping_address_display", r.message[1]) //Address to be displayed on the page } - if(callback){ + if (callback){ return callback(); } } diff --git a/erpnext/public/scss/website.scss b/erpnext/public/scss/website.scss index 735b417da1..617e916724 100644 --- a/erpnext/public/scss/website.scss +++ b/erpnext/public/scss/website.scss @@ -81,4 +81,10 @@ .place-order-container { text-align: right; +} + +.kb-card { + .card-body > .card-title { + line-height: 1.3; + } } \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.js b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.js index 63747afb3f..dac6ac40a7 100644 --- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.js +++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.js @@ -5,21 +5,28 @@ frappe.ui.form.on('Quality Feedback', { refresh: function(frm) { frm.set_value("date", frappe.datetime.get_today()); }, - template: function(frm){ - frappe.call({ - "method": "frappe.client.get", - args: { - doctype: "Quality Feedback Template", - name: frm.doc.template - }, - callback: function(data){ - frm.fields_dict.parameters.grid.remove_all(); - for (var i in data.message.parameters){ - frm.add_child("parameters"); - frm.fields_dict.parameters.get_value()[i].parameter = data.message.parameters[i].parameter; + + template: function(frm) { + if (frm.doc.template) { + frappe.call({ + "method": "frappe.client.get", + args: { + doctype: "Quality Feedback Template", + name: frm.doc.template + }, + callback: function(data) { + if (data && data.message) { + frm.fields_dict.parameters.grid.remove_all(); + + // fetch parameters from template and autofill + for (let template_parameter of data.message.parameters) { + let row = frm.add_child("parameters"); + row.parameter = template_parameter.parameter; + } + frm.refresh(); + } } - frm.refresh(); - } - }); + }); + } } }); diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json index 460438af62..ab9084fa79 100644 --- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json +++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "format:FDBK-{#####}", "creation": "2019-05-26 21:23:05.308379", "doctype": "DocType", @@ -53,12 +54,13 @@ { "fieldname": "document_name", "fieldtype": "Dynamic Link", - "label": "Name", + "label": "Feedback By", "options": "document_type", "reqd": 1 } ], - "modified": "2019-05-28 15:16:01.161662", + "links": [], + "modified": "2020-07-03 15:50:58.589302", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Feedback", diff --git a/erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.json b/erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.json index 31efd04682..bdc9dbab49 100644 --- a/erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.json +++ b/erpnext/quality_management/doctype/quality_feedback_template/quality_feedback_template.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "format:TMPL-{template}", "creation": "2019-05-26 21:17:24.283061", "doctype": "DocType", @@ -30,10 +31,12 @@ "fieldname": "parameters", "fieldtype": "Table", "label": "Parameters", - "options": "Quality Feedback Template Parameter" + "options": "Quality Feedback Template Parameter", + "reqd": 1 } ], - "modified": "2019-05-26 21:48:47.770610", + "links": [], + "modified": "2020-07-03 16:06:03.749415", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Feedback Template", diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json index 0849fd7aeb..7691fe3587 100644 --- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json +++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json @@ -1,10 +1,12 @@ { - "autoname": "format:MTNG-{date}", + "actions": [], + "autoname": "naming_series:", "creation": "2018-10-15 16:25:41.548432", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "naming_series", "date", "cb_00", "status", @@ -53,9 +55,16 @@ "fieldname": "sb_01", "fieldtype": "Section Break", "label": "Minutes" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "MTNG-.YYYY.-.MM.-.DD.-" } ], - "modified": "2019-07-13 19:57:40.500541", + "links": [], + "modified": "2020-05-19 13:18:59.821740", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Meeting", diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js index ded3a51dd6..cf2644e005 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js @@ -2,4 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('Quality Procedure', { + refresh: function(frm) { + frm.set_query("procedure","processes", (frm) =>{ + return { + filters: { + name: ["not in", [frm.parent_quality_procedure, frm.name]] + } + }; + }); + } }); \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json index 6df116c430..b3c0d94890 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_rename": 1, "autoname": "format:PRC-{quality_procedure_name}", "creation": "2018-10-06 00:06:29.756804", "doctype": "DocType", @@ -72,7 +73,7 @@ ], "is_tree": 1, "links": [], - "modified": "2020-03-18 18:09:29.371627", + "modified": "2020-06-17 17:25:03.434953", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Procedure", diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py index d29710dd8e..1952e57867 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py @@ -11,18 +11,18 @@ class QualityProcedure(NestedSet): nsm_parent_field = 'parent_quality_procedure' def before_save(self): - for process in self.processes: - if process.procedure: - doc = frappe.get_doc("Quality Procedure", process.procedure) - if doc.parent_quality_procedure: - frappe.throw(_("{0} already has a Parent Procedure {1}.").format(process.procedure, doc.parent_quality_procedure)) - self.is_group = 1 + self.check_for_incorrect_child() def on_update(self): self.set_parent() def after_insert(self): self.set_parent() + #if Child is Added through Tree View. + if self.parent_quality_procedure: + parent_quality_procedure = frappe.get_doc("Quality Procedure", self.parent_quality_procedure) + parent_quality_procedure.append("processes", {"procedure": self.name}) + parent_quality_procedure.save() def on_trash(self): if self.parent_quality_procedure: @@ -42,11 +42,21 @@ class QualityProcedure(NestedSet): doc.save(ignore_permissions=True) def set_parent(self): + for process in self.processes: + # Set parent for only those children who don't have a parent + parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure") + if not parent_quality_procedure and process.procedure: + frappe.db.set_value(self.doctype, process.procedure, "parent_quality_procedure", self.name) + + def check_for_incorrect_child(self): for process in self.processes: if process.procedure: - doc = frappe.get_doc("Quality Procedure", process.procedure) - doc.parent_quality_procedure = self.name - doc.save(ignore_permissions=True) + # Check if any child process belongs to another parent. + 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")) + self.is_group = 1 @frappe.whitelist() def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=False): diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js index 6df6f656aa..ef48ab6c6e 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure_tree.js @@ -16,6 +16,7 @@ frappe.treeview_settings["Quality Procedure"] = { }, ], breadcrumb: "Setup", + disable_add_node: true, root_label: "All Quality Procedures", get_tree_root: false, menu_items: [ diff --git a/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json b/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json index 0a67fa505e..3925dbb8ac 100644 --- a/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json +++ b/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json @@ -1,6 +1,8 @@ { + "actions": [], "creation": "2019-05-26 00:10:00.248885", "doctype": "DocType", + "editable_grid": 1, "engine": "InnoDB", "field_order": [ "process_description", @@ -23,7 +25,8 @@ } ], "istable": 1, - "modified": "2019-05-26 22:05:49.007189", + "links": [], + "modified": "2020-06-17 15:44:38.937915", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Procedure Process", diff --git a/erpnext/regional/address_template/templates/india.html b/erpnext/regional/address_template/templates/india.html index ffb9d0547e..5d2329efff 100644 --- a/erpnext/regional/address_template/templates/india.html +++ b/erpnext/regional/address_template/templates/india.html @@ -1,7 +1,7 @@ {{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%}{{ city }}
{% if gst_state %}{{ gst_state }}{% endif -%} {% if gst_state_number %}, State Code: {{ gst_state_number }}
{% endif -%} -{% if pincode %}PIN: {{ pincode }}
{% endif -%} +{% if pincode %}Postal Code: {{ pincode }}
{% endif -%} {{ country }}
{% if phone %}Phone: {{ phone }}
{% endif -%} {% if fax %}Fax: {{ fax }}
{% endif -%} diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html index 35f9cf674c..888b2da48e 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html @@ -52,7 +52,7 @@ - (d) {{__("Inward Supplies(liable to reverse charge")}} + (d) {{__("Inward Supplies(liable to reverse charge)")}} {{ flt(data.sup_details.isup_rev.txval, 2) }} {{ flt(data.sup_details.isup_rev.iamt, 2) }} {{ flt(data.sup_details.isup_rev.camt, 2) }} diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.js b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.js index a1cea8f609..c7442667c2 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.js +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.js @@ -3,6 +3,7 @@ frappe.ui.form.on('GSTR 3B Report', { refresh : function(frm) { + frm.doc.__unsaved = 1; if(!frm.is_new()) { frm.set_intro(__("Please save the report again to rebuild or update")); frm.add_custom_button(__('Download JSON'), function() { 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 9e7a023926..2d306ba172 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -158,7 +158,7 @@ class GSTR3BReport(Document): self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"]) self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"]) - self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Registered Regular"], reverse_charge="Y") + self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas"], reverse_charge="Y") self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2) self.set_itc_details(itc_details) @@ -192,31 +192,27 @@ class GSTR3BReport(Document): for d in self.report_dict["itc_elg"]["itc_avl"]: itc_type = itc_type_map.get(d["ty"]) - gst_category = "Registered Regular" + gst_category = ["Registered Regular"] if d["ty"] == 'ISRC': reverse_charge = "Y" + itc_type = 'All Other ITC' + gst_category = ['Unregistered', 'Overseas'] else: reverse_charge = "N" for account_head in self.account_heads: + for category in gst_category: + for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: + d[key[0]] += flt(itc_details.get((category, itc_type, reverse_charge, account_head.get(key[1])), {}).get("amount"), 2) - d["iamt"] += flt(itc_details.get((gst_category, itc_type, reverse_charge, account_head.get('igst_account')), {}).get("amount"), 2) - d["camt"] += flt(itc_details.get((gst_category, itc_type, reverse_charge, account_head.get('cgst_account')), {}).get("amount"), 2) - d["samt"] += flt(itc_details.get((gst_category, itc_type, reverse_charge, account_head.get('sgst_account')), {}).get("amount"), 2) - d["csamt"] += flt(itc_details.get((gst_category, itc_type, reverse_charge, account_head.get('cess_account')), {}).get("amount"), 2) - - net_itc["iamt"] += flt(d["iamt"], 2) - net_itc["camt"] += flt(d["camt"], 2) - net_itc["samt"] += flt(d["samt"], 2) - net_itc["csamt"] += flt(d["csamt"], 2) + for key in ['iamt', 'camt', 'samt', 'csamt']: + net_itc[key] += flt(d[key], 2) for account_head in self.account_heads: itc_inelg = self.report_dict["itc_elg"]["itc_inelg"][1] - itc_inelg["iamt"] = flt(itc_details.get(("Ineligible", "N", account_head.get("igst_account")), {}).get("amount"), 2) - itc_inelg["camt"] = flt(itc_details.get(("Ineligible", "N", account_head.get("cgst_account")), {}).get("amount"), 2) - itc_inelg["samt"] = flt(itc_details.get(("Ineligible", "N", account_head.get("sgst_account")), {}).get("amount"), 2) - itc_inelg["csamt"] = flt(itc_details.get(("Ineligible", "N", account_head.get("cess_account")), {}).get("amount"), 2) + for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: + itc_inelg[key[0]] = flt(itc_details.get(("Ineligible", "N", account_head.get(key[1])), {}).get("amount"), 2) def prepare_data(self, doctype, tax_details, supply_type, supply_category, gst_category_list, reverse_charge="N"): @@ -247,20 +243,15 @@ class GSTR3BReport(Document): osup_det = self.report_dict["sup_details"]["osup_det"] - for d in inter_state_supply.get("Unregistered", []): - self.report_dict["inter_sup"]["unreg_details"].append(d) - osup_det["txval"] = flt(osup_det["txval"] + d["txval"], 2) - osup_det["iamt"] = flt(osup_det["iamt"] + d["iamt"], 2) + for key, value in iteritems(inter_state_supply): + if key[0] == "Unregistered": + self.report_dict["inter_sup"]["unreg_details"].append(value) - for d in inter_state_supply.get("Registered Composition", []): - self.report_dict["inter_sup"]["comp_details"].append(d) - osup_det["txval"] = flt(osup_det["txval"] + d["txval"], 2) - osup_det["iamt"] = flt(osup_det["iamt"] + d["iamt"], 2) + if key[0] == "Registered Composition": + self.report_dict["inter_sup"]["comp_details"].append(value) - for d in inter_state_supply.get("UIN Holders", []): - self.report_dict["inter_sup"]["uin_details"].append(d) - osup_det["txval"] = flt(osup_det["txval"] + d["txval"], 2) - osup_det["iamt"] = flt(osup_det["iamt"] + d["iamt"], 2) + if key[0] == "UIN Holders": + self.report_dict["inter_sup"]["uin_details"].append(value) def get_total_taxable_value(self, doctype, reverse_charge): @@ -274,17 +265,16 @@ class GSTR3BReport(Document): """ #nosec .format(doctype = doctype), (self.month_no, self.year, reverse_charge, self.company, self.gst_details.get("gstin")))) - def get_itc_details(self, reverse_charge='N'): - + def get_itc_details(self): itc_amount = frappe.db.sql(""" select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head, s.eligibility_for_itc, s.reverse_charge from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t - where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s + where s.docstatus = 1 and t.parent = s.name and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s and s.company_gstin = %s group by t.account_head, s.gst_category, s.eligibility_for_itc """, - (reverse_charge, 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 = {} @@ -306,41 +296,55 @@ class GSTR3BReport(Document): (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)[0].total def get_inter_state_supplies(self, state_number): - - inter_state_supply_taxable_value = frappe.db.sql(""" select sum(s.net_total) as total, s.place_of_supply, s.gst_category - from `tabSales Invoice` s where s.docstatus = 1 and month(s.posting_date) = %s and year(s.posting_date) = %s - and s.company = %s and s.company_gstin = %s and s.gst_category in ('Unregistered', 'Registered Composition', 'UIN Holders') - group by s.gst_category, s.place_of_supply""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) - - inter_state_supply_tax = frappe.db.sql(""" select sum(t.tax_amount_after_discount_amount) as tax_amount, s.place_of_supply, s.gst_category - from `tabSales Invoice` s, `tabSales Taxes and Charges` t + inter_state_supply_tax = frappe.db.sql(""" select t.account_head, t.tax_amount_after_discount_amount as tax_amount, + s.name, s.net_total, s.place_of_supply, s.gst_category from `tabSales Invoice` s, `tabSales Taxes and Charges` t where t.parent = s.name and s.docstatus = 1 and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s and s.company_gstin = %s and s.gst_category in ('Unregistered', 'Registered Composition', 'UIN Holders') - group by s.gst_category, s.place_of_supply""", (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) - inter_state_supply_tax_mapping={} + inter_state_supply_tax_mapping = {} inter_state_supply_details = {} for d in inter_state_supply_tax: - inter_state_supply_tax_mapping.setdefault(d.place_of_supply, d.tax_amount) + inter_state_supply_tax_mapping.setdefault(d.name, { + 'place_of_supply': d.place_of_supply, + 'taxable_value': d.net_total, + 'camt': 0.0, + 'samt': 0.0, + 'iamt': 0.0, + 'csamt': 0.0 + }) - for d in inter_state_supply_taxable_value: - inter_state_supply_details.setdefault( - d.gst_category, [] - ) + if d.account_head in [d.cgst_account for d in self.account_heads]: + inter_state_supply_tax_mapping[d.name]['camt'] += d.tax_amount + if d.account_head in [d.sgst_account for d in self.account_heads]: + inter_state_supply_tax_mapping[d.name]['samt'] += d.tax_amount + + if d.account_head in [d.igst_account for d in self.account_heads]: + inter_state_supply_tax_mapping[d.name]['iamt'] += d.tax_amount + + if d.account_head in [d.cess_account for d in self.account_heads]: + inter_state_supply_tax_mapping[d.name]['csamt'] += d.tax_amount + + for key, value in iteritems(inter_state_supply_tax_mapping): if d.place_of_supply: + osup_det = self.report_dict["sup_details"]["osup_det"] + osup_det["txval"] = flt(osup_det["txval"] + value['taxable_value'], 2) + osup_det["iamt"] = flt(osup_det["iamt"] + value['iamt'], 2) + osup_det["camt"] = flt(osup_det["camt"] + value['camt'], 2) + osup_det["samt"] = flt(osup_det["samt"] + value['samt'], 2) + osup_det["csamt"] = flt(osup_det["csamt"] + value['csamt'], 2) + if state_number != d.place_of_supply.split("-")[0]: - inter_state_supply_details[d.gst_category].append({ + inter_state_supply_details.setdefault((d.gst_category, d.place_of_supply), { + "txval": 0.0, "pos": d.place_of_supply.split("-")[0], - "txval": flt(d.total, 2), - "iamt": flt(inter_state_supply_tax_mapping.get(d.place_of_supply), 2) + "iamt": 0.0 }) - else: - osup_det = self.report_dict["sup_details"]["osup_det"] - osup_det["txval"] = flt(osup_det["txval"] + d.total, 2) - osup_det["camt"] = flt(osup_det["camt"] + inter_state_supply_tax_mapping.get(d.place_of_supply)/2, 2) - osup_det["samt"] = flt(osup_det["samt"] + inter_state_supply_tax_mapping.get(d.place_of_supply)/2, 2) + + inter_state_supply_details[(d.gst_category, d.place_of_supply)]['txval'] += value['taxable_value'] + inter_state_supply_details[(d.gst_category, d.place_of_supply)]['iamt'] += value['iamt'] return inter_state_supply_details diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.json b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.json index 59e955c23f..c1680c4b49 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.json +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.json @@ -1,5 +1,4 @@ { - "actions": [], "creation": "2019-10-15 12:33:21.845329", "doctype": "DocType", "editable_grid": 1, @@ -92,8 +91,7 @@ "label": "Upload XML Invoices" } ], - "links": [], - "modified": "2019-12-10 16:37:26.793398", + "modified": "2020-05-25 21:32:49.064579", "modified_by": "Administrator", "module": "Regional", "name": "Import Supplier Invoice", 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 6784ea8a5b..31a7545a0d 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py @@ -58,13 +58,14 @@ class ImportSupplierInvoice(Document): "naming_series": self.invoice_series, "document_type": line.TipoDocumento.text, "bill_date": get_datetime_str(line.Data.text), - "invoice_no": line.Numero.text, + "bill_no": line.Numero.text, "total_discount": 0, "items": [], "buying_price_list": self.default_buying_price_list } - if not invoices_args.get("invoice_no", ''): return + if not invoices_args.get("bill_no", ''): + frappe.throw(_("Numero has not set in the XML file")) supp_dict = get_supplier_details(file_content) invoices_args["destination_code"] = get_destination_code_from_file(file_content) @@ -249,7 +250,7 @@ def create_supplier(supplier_group, args): return existing_supplier_name else: - + new_supplier = frappe.new_doc("Supplier") new_supplier.supplier_name = re.sub('&', '&', args.supplier) new_supplier.supplier_group = supplier_group diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 8593966cc3..290694a789 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -60,6 +60,19 @@ def add_custom_roles_for_reports(): ] )).insert() + 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() + def add_permissions(): for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): add_permission(doctype, 'All', 0) @@ -402,10 +415,45 @@ def make_custom_fields(update=True): 'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], 'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], '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', print_hide=1, - depends_on='eval:doc.salary_mode == "Bank"') + 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 + ), + dict( + 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', diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 732780a0a3..fe7e0c807c 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1,14 +1,16 @@ from __future__ import unicode_literals import frappe, re, json from frappe import _ -from frappe.utils import cstr, flt, date_diff, nowdate +from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words from erpnext.regional.india import states, state_numbers from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.hr.utils import get_salary_assignment -from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip +from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.india import number_state_mapping from six import string_types +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.accounts.utils import get_account_currency def validate_gstin_for_india(doc, method): if hasattr(doc, 'gst_state') and doc.gst_state: @@ -456,19 +458,23 @@ def generate_ewb_json(dt, dn): @frappe.whitelist() def download_ewb_json(): - data = frappe._dict(frappe.local.form_dict) - - frappe.local.response.filecontent = json.dumps(data['data'], indent=4, sort_keys=True) + 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' - billList = json.loads(data['data'])['billLists'] + filename_prefix = 'Bulk' + docname = frappe.local.form_dict.docname + if docname: + if docname.startswith('['): + docname = json.loads(docname) + if len(docname) == 1: + docname = docname[0] - if len(billList) > 1: - doc_name = 'Bulk' - else: - doc_name = data['docname'] + if not isinstance(docname, list): + # removes characters not allowed in a filename (https://stackoverflow.com/a/38766141/4767738) + filename_prefix = re.sub('[^\w_.)( -]', '', docname) - frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(doc_name, 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): @@ -615,8 +621,9 @@ def get_transport_details(data, doc): data.transDocDate = frappe.utils.formatdate(doc.lr_date, 'dd/mm/yyyy') if doc.gst_transporter_id: - validate_gstin_check_digit(doc.gst_transporter_id, label='GST Transporter ID') - data.transporterId = doc.gst_transporter_id + if doc.gst_transporter_id[0:2] != "88": + validate_gstin_check_digit(doc.gst_transporter_id, label='GST Transporter ID') + data.transporterId = doc.gst_transporter_id return data @@ -641,6 +648,7 @@ def validate_state_code(state_code, address): else: return int(state_code) +@frappe.whitelist() def get_gst_accounts(company, account_wise=False): gst_accounts = frappe._dict() gst_settings_accounts = frappe.get_all("GST Account", @@ -657,5 +665,79 @@ def get_gst_accounts(company, account_wise=False): elif val: gst_accounts[val] = acc - return gst_accounts + +def update_grand_total_for_rcm(doc, method): + country = frappe.get_cached_value('Company', doc.company, 'country') + + if country != 'India': + return + + if doc.reverse_charge == 'Y': + gst_accounts = get_gst_accounts(doc.company) + gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ + + gst_accounts.get('igst_account') + + gst_tax = 0 + for tax in doc.get('taxes'): + if tax.category not in ("Total", "Valuation and Total"): + continue + + if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: + gst_tax += tax.base_tax_amount_after_discount_amount + + doc.taxes_and_charges_added -= gst_tax + doc.total_taxes_and_charges -= gst_tax + + update_totals(gst_tax, doc) + +def update_totals(gst_tax, doc): + doc.grand_total -= gst_tax + + if doc.meta.get_field("rounded_total"): + if doc.is_rounded_total_disabled(): + 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.outstanding_amount = doc.rounded_total or doc.grand_total + + doc.in_words = money_in_words(doc.grand_total, doc.currency) + doc.set_payment_schedule() + +def make_regional_gl_entries(gl_entries, doc): + country = frappe.get_cached_value('Company', doc.company, 'country') + + if country != 'India': + return + + if doc.reverse_charge == 'Y': + gst_accounts = get_gst_accounts(doc.company) + gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ + + gst_accounts.get('igst_account') + + for tax in doc.get('taxes'): + if tax.category not in ("Total", "Valuation and Total"): + continue + + if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: + 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, + "credit": tax.base_tax_amount_after_discount_amount, + "credits_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 \ No newline at end of file diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 2d0ad66b0a..6ab73413df 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -7,11 +7,13 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from frappe.permissions import add_permission, update_permission_property from erpnext.regional.italy import fiscal_regimes, tax_exemption_reasons, mode_of_payment_codes, vat_collectability_options def setup(company=None, patch=True): make_custom_fields() setup_report() + add_permissions() def make_custom_fields(update=True): invoice_item_fields = [ @@ -200,3 +202,21 @@ def setup_report(): dict(role='Accounts Manager') ] )).insert() + +def add_permissions(): + doctype = 'Import Supplier Invoice' + add_permission(doctype, 'All', 0) + + 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) + + 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) \ No newline at end of file diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js index d8638ab05d..55f12cf373 100644 --- a/erpnext/regional/report/datev/datev.js +++ b/erpnext/regional/report/datev/datev.js @@ -30,7 +30,7 @@ frappe.query_reports["DATEV"] = { } ], onload: function(query_report) { - query_report.page.add_inner_button("Download DATEV Export", () => { + query_report.page.add_menu_item(__("Download DATEV File"), () => { const filters = JSON.stringify(query_report.get_values()); window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`); }); diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index a8e40cc493..7fec94e740 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -8,17 +8,18 @@ Provide a report and downloadable CSV according to the German DATEV format. all required columns. Used to import the data into the DATEV Software. """ from __future__ import unicode_literals + import datetime import json -import zlib import zipfile import six +import frappe +import pandas as pd + +from frappe import _ from csv import QUOTE_NONNUMERIC from six import BytesIO from six import string_types -import frappe -from frappe import _ -import pandas as pd from .datev_constants import DataCategory from .datev_constants import Transactions from .datev_constants import DebtorsCreditors @@ -130,8 +131,10 @@ def get_customers(filters): SELECT acc.account_number as 'Konto', - cus.customer_name as 'Name (Adressatentyp Unternehmen)', - case cus.customer_type when 'Individual' then 1 when 'Company' then 2 else 0 end as 'Adressatentyp', + CASE cus.customer_type WHEN 'Company' THEN cus.customer_name ELSE null END as 'Name (Adressatentyp Unternehmen)', + CASE cus.customer_type WHEN 'Individual' THEN con.last_name ELSE null END as 'Name (Adressatentyp natürl. Person)', + CASE cus.customer_type WHEN 'Individual' THEN con.first_name ELSE null END as 'Vorname (Adressatentyp natürl. Person)', + CASE cus.customer_type WHEN 'Individual' THEN '1' WHEN 'Company' THEN '2' ELSE '0' end as 'Adressatentyp', adr.address_line1 as 'Straße', adr.pincode as 'Postleitzahl', adr.city as 'Ort', @@ -140,8 +143,7 @@ def get_customers(filters): con.email_id as 'E-Mail', coalesce(con.mobile_no, con.phone) as 'Telefon', cus.website as 'Internet', - cus.tax_id as 'Steuernummer', - ccl.credit_limit as 'Kreditlimit (Debitor)' + cus.tax_id as 'Steuernummer' FROM `tabParty Account` par @@ -160,10 +162,6 @@ def get_customers(filters): left join `tabContact` con on con.name = cus.customer_primary_contact - left join `tabCustomer Credit Limit` ccl - on ccl.parent = cus.name - and ccl.company = par.company - WHERE par.company = %(company)s AND par.parenttype = 'Customer'""", filters, as_dict=1) @@ -179,8 +177,10 @@ def get_suppliers(filters): SELECT acc.account_number as 'Konto', - sup.supplier_name as 'Name (Adressatentyp Unternehmen)', - case sup.supplier_type when 'Individual' then '1' when 'Company' then '2' else '0' end as 'Adressatentyp', + CASE sup.supplier_type WHEN 'Company' THEN sup.supplier_name ELSE null END as 'Name (Adressatentyp Unternehmen)', + CASE sup.supplier_type WHEN 'Individual' THEN con.last_name ELSE null END as 'Name (Adressatentyp natürl. Person)', + CASE sup.supplier_type WHEN 'Individual' THEN con.first_name ELSE null END as 'Vorname (Adressatentyp natürl. Person)', + CASE sup.supplier_type WHEN 'Individual' THEN '1' WHEN 'Company' THEN '2' ELSE '0' end as 'Adressatentyp', adr.address_line1 as 'Straße', adr.pincode as 'Postleitzahl', adr.city as 'Ort', @@ -226,9 +226,18 @@ def get_suppliers(filters): def get_account_names(filters): - return frappe.get_list("Account", - fields=["account_number as Konto", "name as Kontenbeschriftung"], - filters={"company": filters.get("company"), "is_group": "0"}) + return frappe.db.sql(""" + SELECT + + account_number as 'Konto', + LEFT(account_name, 40) as 'Kontenbeschriftung', + 'de-DE' as 'Sprach-ID' + + FROM `tabAccount` + WHERE company = %(company)s + AND is_group = 0 + AND account_number != '' + """, filters, as_dict=1) def get_datev_csv(data, filters, csv_class): @@ -287,9 +296,7 @@ def get_datev_csv(data, filters, csv_class): def get_header(filters, csv_class): - coa = frappe.get_value("Company", filters.get("company"), "chart_of_accounts") - description = filters.get("voucher_type", csv_class.FORMAT_NAME) - coa_used = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") + description = filters.get('voucher_type', csv_class.FORMAT_NAME) header = [ # DATEV format @@ -316,19 +323,19 @@ def get_header(filters, csv_class): # J = Imported by -- stays empty '', # K = Tax consultant number (Beraternummer) - frappe.get_value("DATEV Settings", filters.get("company"), "consultant_number"), + filters.get('consultant_number', '0000000'), # L = Tax client number (Mandantennummer) - frappe.get_value("DATEV Settings", filters.get("company"), "client_number"), + filters.get('client_number', '00000'), # M = Start of the fiscal year (Wirtschaftsjahresbeginn) frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"), # N = Length of account numbers (Sachkontenlänge) - '4', + '%d' % filters.get('acc_len', 4), # O = Transaction batch start date (YYYYMMDD) - frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd"), + 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"), + 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)), + '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', # R = Diktatkürzel '', # S = Buchungstyp @@ -343,12 +350,12 @@ def get_header(filters, csv_class): # 40 = Kalkulatorik # 11 = Reserviert # 12 = Reserviert - '0', + '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', # V = Default currency, for example, "EUR" - '"%s"' % frappe.get_value("Company", filters.get("company"), "default_currency"), + '"%s"' % filters.get('default_currency', 'EUR') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', # reserviert '', # Derivatskennzeichen @@ -358,7 +365,7 @@ def get_header(filters, csv_class): # reserviert '', # SKR - '"%s"' % coa_used, + '"%s"' % filters.get('skr', '04'), # Branchen-Lösungs-ID '', # reserviert @@ -389,6 +396,18 @@ def download_datev_csv(filters=None): validate(filters) + # set chart of accounts used + coa = frappe.get_value('Company', filters.get('company'), 'chart_of_accounts') + filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') + + # set account number length + account_numbers = frappe.get_list('Account', fields=['account_number'], filters={'is_group': 0, 'account_number': ('!=', '')}) + filters['acc_len'] = max([len(a.account_number) for a in account_numbers]) + + filters['consultant_number'] = frappe.get_value('DATEV Settings', filters.get('company'), 'consultant_number') + filters['client_number'] = frappe.get_value('DATEV Settings', filters.get('company'), 'client_number') + filters['default_currency'] = frappe.get_value('Company', filters.get('company'), 'default_currency') + # This is where my zip will be written zip_buffer = BytesIO() # This is my zip file diff --git a/erpnext/regional/report/datev/datev_constants.py b/erpnext/regional/report/datev/datev_constants.py index a059ed365a..e063703005 100644 --- a/erpnext/regional/report/datev/datev_constants.py +++ b/erpnext/regional/report/datev/datev_constants.py @@ -465,60 +465,71 @@ QUERY_REPORT_COLUMNS = [ "label": "Umsatz (ohne Soll/Haben-Kz)", "fieldname": "Umsatz (ohne Soll/Haben-Kz)", "fieldtype": "Currency", + "width": 100 }, { "label": "Soll/Haben-Kennzeichen", "fieldname": "Soll/Haben-Kennzeichen", "fieldtype": "Data", + "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": "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" + "options": "DocType", + "width": 100 }, { "label": "Beleginfo - Inhalt 1", "fieldname": "Beleginfo - Inhalt 1", "fieldtype": "Dynamic Link", - "options": "Beleginfo - Art 1" + "options": "Beleginfo - Art 1", + "width": 150 }, { "label": "Beleginfo - Art 2", "fieldname": "Beleginfo - Art 2", "fieldtype": "Link", - "options": "DocType" + "options": "DocType", + "width": 100 }, { "label": "Beleginfo - Inhalt 2", "fieldname": "Beleginfo - Inhalt 2", "fieldtype": "Dynamic Link", - "options": "Beleginfo - Art 2" + "options": "Beleginfo - Art 2", + "width": 150 } ] diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py index 3cc65fe9d3..eed62a8690 100644 --- a/erpnext/regional/report/datev/test_datev.py +++ b/erpnext/regional/report/datev/test_datev.py @@ -90,7 +90,7 @@ def make_customer_with_account(customer_name, company): if not frappe.db.exists("Customer", customer_name): customer = frappe.get_doc({ - "doctype": "Customer", + "doctype": "Customer", "customer_name": customer_name, "customer_type": "Company", "accounts": [{ @@ -155,17 +155,17 @@ class TestDatev(TestCase): setup_fiscal_year() warehouse = frappe.db.get_value("Item Default", { - "parent": item.name, + "parent": item.name, "company": self.company.name }, "default_warehouse") income_account = frappe.db.get_value("Account", { - "account_number": "4200", + "account_number": "4200", "company": self.company.name }, "name") tax_account = frappe.db.get_value("Account", { - "account_number": "3806", + "account_number": "3806", "company": self.company.name }, "name") @@ -186,9 +186,12 @@ class TestDatev(TestCase): "charge_type": "On Net Total", "account_head": tax_account, "description": "Umsatzsteuer 19 %", - "rate": 19 + "rate": 19, + "cost_center": self.company.cost_center }) + si.cost_center = self.company.cost_center + si.save() si.submit() @@ -196,7 +199,7 @@ class TestDatev(TestCase): def is_subset(get_data, allowed_keys): """ Validate that the dict contains only allowed keys. - + Params: get_data -- Function that returns a list of dicts. allowed_keys -- List of allowed keys diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index fd1cc58c20..8885b88c2a 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -117,12 +117,20 @@ class Gstr1Report(object): else: row.append(invoice_details.get(fieldname)) taxable_value = 0 + + if invoice in self.cgst_sgst_invoices: + division_factor = 2 + else: + division_factor = 1 + 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 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) + 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, []): + 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) row += [tax_rate or 0, taxable_value] @@ -196,7 +204,7 @@ class Gstr1Report(object): if d.item_code not in self.invoice_items.get(d.parent, {}): self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, sum(i.get('base_net_amount', 0) for i in items - if i.item_code == d.item_code and i.parent == d.parent)) + if i.item_code == d.item_code and i.parent == d.parent)) item_tax_rate = {} @@ -221,6 +229,8 @@ class Gstr1Report(object): self.items_based_on_tax_rate = {} self.invoice_cess = frappe._dict() + self.cgst_sgst_invoices = [] + unidentified_gst_accounts = [] for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: if account in self.gst_accounts.cess_account: @@ -243,6 +253,8 @@ class Gstr1Report(object): tax_rate = tax_amounts[0] if cgst_or_sgst: tax_rate *= 2 + 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, []) diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py index f326fe07ca..f899349ccc 100644 --- a/erpnext/regional/report/gstr_2/gstr_2.py +++ b/erpnext/regional/report/gstr_2/gstr_2.py @@ -44,30 +44,30 @@ 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 inv not in self.igst_invoices: - rate = rate / 2 - row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) - tax_amount = taxable_value * rate / 100 - row += [0, tax_amount, tax_amount] - else: - row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) - tax_amount = taxable_value * rate / 100 - row += [tax_amount, 0, 0] + if rate: + if inv not in self.igst_invoices: + rate = rate / 2 + row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) + tax_amount = taxable_value * rate / 100 + row += [0, tax_amount, tax_amount] + else: + row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) + tax_amount = taxable_value * rate / 100 + row += [tax_amount, 0, 0] + 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') + ] + 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") - 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') - ] - 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") - - self.data.append(row) + self.data.append(row) def get_igst_invoices(self): self.igst_invoices = [] @@ -86,7 +86,7 @@ class Gstr2Report(Gstr1Report): conditions += opts[1] if self.filters.get("type_of_business") == "B2B": - conditions += "and ifnull(gst_category, '') != 'Overseas' and is_return != 1 " + conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') and is_return != 1 " elif self.filters.get("type_of_business") == "CDNR": conditions += """ and is_return = 1 """ diff --git a/erpnext/regional/report/professional_tax_deductions/__init__.py b/erpnext/regional/report/professional_tax_deductions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.js b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.js new file mode 100644 index 0000000000..29c7dbf43c --- /dev/null +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.js @@ -0,0 +1,7 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { + frappe.query_reports["Professional Tax Deductions"] = erpnext.salary_slip_deductions_report_filters; +}); \ No newline at end of file diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.json b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.json new file mode 100644 index 0000000000..9938e9db52 --- /dev/null +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.json @@ -0,0 +1,20 @@ +{ + "add_total_row": 0, + "creation": "2020-06-02 00:37:44.537355", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-06-16 19:02:26.306348", + "modified_by": "Administrator", + "module": "Regional", + "name": "Professional Tax Deductions", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Salary Slip", + "report_name": "Professional Tax Deductions", + "report_type": "Script Report", + "roles": [] +} \ No newline at end of file diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py new file mode 100644 index 0000000000..acde68a942 --- /dev/null +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py @@ -0,0 +1,72 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from erpnext.regional.report.provident_fund_deductions.provident_fund_deductions import get_conditions + +def execute(filters=None): + data = get_data(filters) + columns = get_columns(filters) if len(data) else [] + + return columns, data + +def get_columns(filters): + columns = [ + { + "label": _("Employee"), + "options": "Employee", + "fieldname": "employee", + "fieldtype": "Link", + "width": 200 + }, + { + "label": _("Employee Name"), + "options": "Employee", + "fieldname": "employee_name", + "fieldtype": "Link", + "width": 160 + }, + { + "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' """)) + + 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 + 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) + + for d in entry: + + employee = { + "employee": d.employee, + "employee_name": d.employee_name, + "amount": d.amount + } + + data.append(employee) + + return data \ No newline at end of file diff --git a/erpnext/regional/report/provident_fund_deductions/__init__.py b/erpnext/regional/report/provident_fund_deductions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.js b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.js new file mode 100644 index 0000000000..b4dc28d177 --- /dev/null +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.js @@ -0,0 +1,7 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { + frappe.query_reports["Provident Fund Deductions"] = erpnext.salary_slip_deductions_report_filters; +}); \ No newline at end of file diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.json b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.json new file mode 100644 index 0000000000..e25d335f9b --- /dev/null +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.json @@ -0,0 +1,20 @@ +{ + "add_total_row": 0, + "creation": "2020-06-01 23:44:07.919117", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-06-16 18:54:19.305763", + "modified_by": "Administrator", + "module": "Regional", + "name": "Provident Fund Deductions", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Salary Slip", + "report_name": "Provident Fund Deductions", + "report_type": "Script Report", + "roles": [] +} \ No newline at end of file diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py new file mode 100644 index 0000000000..597072c53a --- /dev/null +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py @@ -0,0 +1,168 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import getdate +from frappe import _ + +def execute(filters=None): + data = get_data(filters) + columns = get_columns(filters) if len(data) else [] + + return columns, data + +def get_columns(filters): + columns = [ + { + "label": _("Employee"), + "options": "Employee", + "fieldname": "employee", + "fieldtype": "Link", + "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 + }, + { + "label": _("Additional PF"), + "fieldname": "additional_pf", + "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"]) ) + + if filters.get("branch"): + conditions.append("sal.branch = '%s' " % (filters["branch"]) ) + + if filters.get("company"): + conditions.append("sal.company = '%s' " % (filters["company"])) + + if filters.get("month"): + conditions.append("month(sal.start_date) = '%s' " % (filters["month"])) + + if filters.get("year"): + conditions.append("year(start_date) = '%s' " % (filters["year"])) + + if filters.get("mode_of_payment"): + conditions.append("sal.mode_of_payment = '%s' " % (filters["mode_of_payment"])) + + return " and ".join(conditions) + +def prepare_data(entry,component_type_dict): + data_list = {} + + employee_account_dict = frappe._dict(frappe.db.sql(""" select name, provident_fund_account from `tabEmployee`""")) + + for d in entry: + + component_type = component_type_dict.get(d.salary_component) + + 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 + }) + + return data_list + +def get_data(filters): + data = [] + + conditions = get_conditions(filters) + + salary_slips = frappe.db.sql(""" select sal.name from `tabSalary Slip` sal + where docstatus = 1 %s + """ % (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')""")) + + if not len(component_type_dict): + return [] + + 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) + + data_list = prepare_data(entry,component_type_dict) + + for d in salary_slips: + total = 0 + if data_list.get(d.name): + 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") + } + + if data_list.get(d.name).get("Provident Fund"): + employee["pf_amount"] = data_list.get(d.name).get("Provident Fund") + total += data_list.get(d.name).get("Provident Fund") + + if data_list.get(d.name).get("Additional Provident Fund"): + employee["additional_pf"] = data_list.get(d.name).get("Additional Provident Fund") + total += data_list.get(d.name).get("Additional Provident Fund") + + if data_list.get(d.name).get("Provident Fund Loan"): + employee["pf_loan"] = data_list.get(d.name).get("Provident Fund Loan") + total += data_list.get(d.name).get("Provident Fund Loan") + + employee["total"] = total + + data.append(employee) + + 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""") + if not year_list: + year_list = [getdate().year] + + return "\n".join(str(year) for year in year_list) \ No newline at end of file diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index a01c6ceec3..a0425f6b1c 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals +import frappe from frappe.utils import flt from erpnext.controllers.taxes_and_totals import get_itemised_tax +from six import iteritems def update_itemised_tax_data(doc): if not doc.taxes: return @@ -9,7 +11,17 @@ def update_itemised_tax_data(doc): for row in doc.items: tax_rate = 0.0 - if itemised_tax.get(row.item_code): + item_tax_rate = 0.0 + + if row.item_tax_rate: + item_tax_rate = frappe.parse_json(row.item_tax_rate) + + # First check if tax rate is present + # If not then look up in item_wise_tax_detail + if item_tax_rate: + 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()]) row.tax_rate = flt(tax_rate, row.precision("tax_rate")) diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index 6d344025d2..2b0ecafebc 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -9,14 +9,30 @@ def setup(company=None, patch=True): make_custom_fields() add_print_formats() -def make_custom_fields(): +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?') + ], + '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?') + ], + '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?') ] } - create_custom_fields(custom_fields) + create_custom_fields(custom_fields, update=update) def add_print_formats(): frappe.reload_doc("regional", "print_format", "irs_1099_form") 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 a748f9a007..357deaac00 100644 --- a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py +++ b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py @@ -65,6 +65,7 @@ def make_invoice(table, customer, mode_of_payment): 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']) @@ -84,4 +85,4 @@ def get_restaurant_and_menu_name(table): if not menu: frappe.throw(_('Please set an active menu for Restaurant {0}').format(restaurant)) - return restaurant, menu \ No newline at end of file + return restaurant, menu diff --git a/erpnext/selling/dashboard_fixtures.py b/erpnext/selling/dashboard_fixtures.py new file mode 100644 index 0000000000..889cb88dce --- /dev/null +++ b/erpnext/selling/dashboard_fixtures.py @@ -0,0 +1,198 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import json +from frappe import _ +from frappe.utils import nowdate +from erpnext.accounts.utils import get_fiscal_year + +def get_data(): + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(), + "number_cards": get_number_cards(), + }) + +def get_company_for_dashboards(): + company = frappe.defaults.get_defaults().company + if company: + return company + else: + company_list = frappe.get_list("Company") + if company_list: + return company_list[0].name + return None + +company = frappe.get_doc("Company", get_company_for_dashboards()) +fiscal_year = get_fiscal_year(nowdate(), as_dict=1) +fiscal_year_name = fiscal_year.get("name") +start_date = str(fiscal_year.get("year_start_date")) +end_date = str(fiscal_year.get("year_end_date")) + +def get_dashboards(): + return [{ + "name": "Selling", + "dashboard_name": "Selling", + "charts": [ + { "chart": "Sales Order Trends", "width": "Full"}, + { "chart": "Top Customers", "width": "Half"}, + { "chart": "Sales Order Analysis", "width": "Half"}, + { "chart": "Item-wise Annual Sales", "width": "Full"} + ], + "cards": [ + { "card": "Annual Sales"}, + { "card": "Sales Orders to Deliver"}, + { "card": "Sales Orders to Bill"}, + { "card": "Active Customers"} + ] + }] + +def get_charts(): + return [ + { + "name": "Sales Order Analysis", + "chart_name": _("Sales Order Analysis"), + "chart_type": "Report", + "custom_options": json.dumps({ + "type": "donut", + "height": 300, + "axisOptions": {"shortenYAxisNumbers": 1} + }), + "doctype": "Dashboard Chart", + "filters_json": json.dumps({ + "company": company.name, + "from_date": start_date, + "to_date": end_date + }), + "is_custom": 1, + "is_public": 1, + "owner": "Administrator", + "report_name": "Sales Order Analysis", + "type": "Donut" + }, + { + "name": "Item-wise Annual Sales", + "chart_name": _("Item-wise Annual Sales"), + "chart_type": "Report", + "doctype": "Dashboard Chart", + "filters_json": json.dumps({ + "company": company.name, + "from_date": start_date, + "to_date": end_date + }), + "is_custom": 1, + "is_public": 1, + "owner": "Administrator", + "report_name": "Item-wise Sales History", + "type": "Bar" + }, + { + "name": "Sales Order Trends", + "chart_name": _("Sales Order Trends"), + "chart_type": "Report", + "custom_options": json.dumps({ + "type": "line", + "axisOptions": {"shortenYAxisNumbers": 1}, + "tooltipOptions": {}, + "lineOptions": { + "regionFill": 1 + } + }), + "doctype": "Dashboard Chart", + "filters_json": json.dumps({ + "company": company.name, + "period": "Monthly", + "fiscal_year": fiscal_year_name, + "based_on": "Item" + }), + "is_custom": 1, + "is_public": 1, + "owner": "Administrator", + "report_name": "Sales Order Trends", + "type": "Line" + }, + { + "name": "Top Customers", + "chart_name": _("Top Customers"), + "chart_type": "Report", + "doctype": "Dashboard Chart", + "filters_json": json.dumps({ + "company": company.name, + "period": "Monthly", + "fiscal_year": fiscal_year_name, + "based_on": "Customer" + }), + "is_custom": 1, + "is_public": 1, + "owner": "Administrator", + "report_name": "Delivery Note Trends", + "type": "Bar" + } + ] + +def get_number_cards(): + return [ + { + "name": "Annual Sales", + "aggregate_function_based_on": "base_net_total", + "doctype": "Number Card", + "document_type": "Sales Order", + "filters_json": json.dumps([ + ["Sales Order", "transaction_date", "Between", [start_date, end_date], False], + ["Sales Order", "status", "not in", ["Draft", "Cancelled", "Closed", None], False], + ["Sales Order", "docstatus", "=", 1, False], + ["Sales Order", "company", "=", company.name, False] + ]), + "function": "Sum", + "is_public": 1, + "label": _("Annual Sales"), + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "name": "Sales Orders to Deliver", + "doctype": "Number Card", + "document_type": "Sales Order", + "filters_json": json.dumps([ + ["Sales Order", "status", "in", ["To Deliver and Bill", "To Deliver", None], False], + ["Sales Order", "docstatus", "=", 1, False], + ["Sales Order", "company", "=", company.name, False] + ]), + "function": "Count", + "is_public": 1, + "label": _("Sales Orders to Deliver"), + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Weekly" + }, + { + "name": "Sales Orders to Bill", + "doctype": "Number Card", + "document_type": "Sales Order", + "filters_json": json.dumps([ + ["Sales Order", "status", "in", ["To Deliver and Bill", "To Bill", None], False], + ["Sales Order", "docstatus", "=", 1, False], + ["Sales Order", "company", "=", company.name, False] + ]), + "function": "Count", + "is_public": 1, + "label": _("Sales Orders to Bill"), + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Weekly" + }, + { + "name": "Active Customers", + "doctype": "Number Card", + "document_type": "Customer", + "filters_json": json.dumps([["Customer", "disabled", "=", "0"]]), + "function": "Count", + "is_public": 1, + "label": "Active Customers", + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + } + ] \ No newline at end of file diff --git a/erpnext/selling/desk_page/selling/selling.json b/erpnext/selling/desk_page/selling/selling.json index a20806b264..225238233a 100644 --- a/erpnext/selling/desk_page/selling/selling.json +++ b/erpnext/selling/desk_page/selling/selling.json @@ -1,5 +1,10 @@ { "cards": [ + { + "hidden": 0, + "label": "Selling", + "links": "[\n {\n \"description\": \"Customer Database.\",\n \"label\": \"Customer\",\n \"name\": \"Customer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Quotes to Leads or Customers.\",\n \"label\": \"Quotation\",\n \"name\": \"Quotation\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Confirmed orders from Customers.\",\n \"label\": \"Sales Order\",\n \"name\": \"Sales Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Sales Invoice\",\n \"name\": \"Sales Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Blanket Orders from Costumers.\",\n \"label\": \"Blanket Order\",\n \"name\": \"Blanket Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Manage Sales Partners.\",\n \"label\": \"Sales Partner\",\n \"name\": \"Sales Partner\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Manage Sales Person Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Sales Person\",\n \"link\": \"Tree/Sales Person\",\n \"name\": \"Sales Person\",\n \"type\": \"doctype\"\n }\n]" + }, { "hidden": 0, "label": "Items and Pricing", @@ -10,76 +15,78 @@ "label": "Settings", "links": "[\n {\n \"description\": \"Default settings for selling transactions.\",\n \"label\": \"Selling Settings\",\n \"name\": \"Selling Settings\",\n \"settings\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Template of terms or contract.\",\n \"label\": \"Terms and Conditions Template\",\n \"name\": \"Terms and Conditions\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tax template for selling transactions.\",\n \"label\": \"Sales Taxes and Charges Template\",\n \"name\": \"Sales Taxes and Charges Template\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Track Leads by Lead Source.\",\n \"label\": \"Lead Source\",\n \"name\": \"Lead Source\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Customer Group Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Customer Group\",\n \"link\": \"Tree/Customer Group\",\n \"name\": \"Customer Group\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Contacts.\",\n \"label\": \"Contact\",\n \"name\": \"Contact\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Addresses.\",\n \"label\": \"Address\",\n \"name\": \"Address\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Territory Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Territory\",\n \"link\": \"Tree/Territory\",\n \"name\": \"Territory\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Sales campaigns.\",\n \"label\": \"Campaign\",\n \"name\": \"Campaign\",\n \"type\": \"doctype\"\n }\n]" }, - { - "hidden": 0, - "label": "Other Reports", - "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Customer Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"route_options\": {\n \"party_type\": \"Customer\"\n },\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"BOM\"\n ],\n \"doctype\": \"BOM\",\n \"is_query_report\": true,\n \"label\": \"BOM Search\",\n \"name\": \"BOM Search\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Available Stock for Packing Items\",\n \"name\": \"Available Stock for Packing Items\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Pending SO Items For Purchase Request\",\n \"name\": \"Pending SO Items For Purchase Request\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customers Without Any Sales Transactions\",\n \"name\": \"Customers Without Any Sales Transactions\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n }\n]" - }, - { - "hidden": 0, - "label": "Sales", - "links": "[\n {\n \"description\": \"Customer Database.\",\n \"label\": \"Customer\",\n \"name\": \"Customer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Quotes to Leads or Customers.\",\n \"label\": \"Quotation\",\n \"name\": \"Quotation\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Confirmed orders from Customers.\",\n \"label\": \"Sales Order\",\n \"name\": \"Sales Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Invoices for Costumers.\",\n \"label\": \"Sales Invoice\",\n \"name\": \"Sales Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Blanket Orders from Costumers.\",\n \"label\": \"Blanket Order\",\n \"name\": \"Blanket Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Manage Sales Partners.\",\n \"label\": \"Sales Partner\",\n \"name\": \"Sales Partner\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Manage Sales Person Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Sales Person\",\n \"link\": \"Tree/Sales Person\",\n \"name\": \"Sales Person\",\n \"type\": \"doctype\"\n }\n]" - }, { "hidden": 0, "label": "Key Reports", - "links": "[\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Ordered Items To Be Delivered\",\n \"name\": \"Ordered Items To Be Delivered\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n }\n]" + }, + { + "hidden": 0, + "label": "Other Reports", + "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Customer Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"route_options\": {\n \"party_type\": \"Customer\"\n },\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Available Stock for Packing Items\",\n \"name\": \"Available Stock for Packing Items\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Pending SO Items For Purchase Request\",\n \"name\": \"Pending SO Items For Purchase Request\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Invoice Trends\",\n \"name\": \"Sales Invoice Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customers Without Any Sales Transactions\",\n \"name\": \"Customers Without Any Sales Transactions\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", "charts": [ { - "chart_name": "Income", - "label": "Income" + "chart_name": "Sales Order Trends", + "label": "Sales Order Trends" } ], + "charts_label": "Selling ", "creation": "2020-01-28 11:49:12.092882", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", + "hide_custom": 1, "idx": 0, "is_standard": 1, "label": "Selling", - "modified": "2020-04-01 11:28:51.047373", + "modified": "2020-06-29 19:26:35.139097", "modified_by": "Administrator", "module": "Selling", "name": "Selling", + "onboarding": "Selling", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "shortcuts": [ { - "label": "Sales Invoice", - "link_to": "Sales Invoice", + "color": "#cef6d1", + "format": "{} Available", + "label": "Item", + "link_to": "Item", + "stats_filter": "{\n \"disabled\":0\n}", "type": "DocType" }, { + "color": "#ffe8cd", + "format": "{} To Deliver", "label": "Sales Order", "link_to": "Sales Order", + "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\":[\"in\", [\"To Deliver\", \"To Deliver and Bill\"]]\n}", "type": "DocType" }, { - "label": "Quotation", - "link_to": "Quotation", - "type": "DocType" - }, - { - "label": "Delivery Note", - "link_to": "Delivery Note", - "type": "DocType" - }, - { - "label": "Accounts Receivable", - "link_to": "Accounts Receivable", + "color": "#cef6d1", + "format": "{} Open", + "label": "Sales Analytics", + "link_to": "Sales Analytics", + "stats_filter": "{ \"Status\": \"Open\" }", "type": "Report" }, { - "label": "Sales Register", - "link_to": "Sales Register", + "label": "Sales Order Analysis", + "link_to": "Sales Order Analysis", "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "Selling", + "type": "Dashboard" } - ] + ], + "shortcuts_label": "Quick Access" } \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 3d172ac7a2..e614acdb82 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -3,16 +3,19 @@ from __future__ import unicode_literals import frappe +import json from frappe.model.naming import set_name_by_naming_series -from frappe import _, msgprint, throw +from frappe import _, msgprint import frappe.defaults -from frappe.utils import flt, cint, cstr, today +from frappe.utils import flt, cint, cstr, today, get_formatted_email from frappe.desk.reportview import build_match_conditions, get_filters_cond from erpnext.utilities.transaction_base import TransactionBase from erpnext.accounts.party import validate_party_accounts, get_dashboard_info, get_timeline_data # keep this from frappe.contacts.address_and_contact import load_address_and_contact, delete_contact_and_address from frappe.model.rename_doc import update_linked_doctypes from frappe.model.mapper import get_mapped_doc +from frappe.utils.user import get_users_with_role + class Customer(TransactionBase): def get_feed(self): @@ -336,11 +339,15 @@ def get_loyalty_programs(doc): return lp_details +@frappe.whitelist() 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": fields = ["name", "customer_group", "territory"] - else: - fields = ["name", "customer_name", "customer_group", "territory"] + + fields = get_fields("Customer", fields) match_conditions = build_match_conditions("Customer") match_conditions = "and {}".format(match_conditions) if match_conditions else "" @@ -349,14 +356,17 @@ 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("""select %s from `tabCustomer` where docstatus < 2 - and (%s like %s or customer_name like %s) - {match_conditions} + return frappe.db.sql(""" + select %s + from `tabCustomer` + where docstatus < 2 + and (%s like %s or customer_name like %s) + {match_conditions} order by - 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"), + 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)) @@ -371,10 +381,44 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, .format(customer, customer_outstanding, credit_limit)) # If not authorized person raise exception - credit_controller = frappe.db.get_value('Accounts Settings', None, 'credit_controller') - if not credit_controller or credit_controller not in frappe.get_roles(): - throw(_("Please contact to the user who have Sales Master Manager {0} role") - .format(" / " + credit_controller if credit_controller else "")) + 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 = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] + + if not credit_controller_users: + 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)) + + # 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, + 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 + } + } + ) + +@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) def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): # Outstanding based on GL Entries @@ -497,6 +541,7 @@ def make_address(args, is_primary_address=1): return address +@frappe.whitelist() def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters): customer = filters.get('customer') return frappe.db.sql(""" @@ -507,4 +552,4 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil """, { 'customer': customer, 'txt': '%%%s%%' % txt - }) + }) \ No newline at end of file diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index c8a71677f9..0c85a1b53c 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -22,12 +22,13 @@ class ProductBundle(Document): """Validates, main Item is not a stock item""" if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"): frappe.throw(_("Parent Item {0} must not be a Stock Item").format(self.new_item_code)) - + def validate_child_items(self): for item in self.items: if frappe.db.exists("Product Bundle", item.item_code): - frappe.throw(_("Child Item should not be a Product Bundle. Please remove item `{0}` and save").format(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() def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond diff --git a/erpnext/selling/doctype/product_bundle/test_product_bundle.py b/erpnext/selling/doctype/product_bundle/test_product_bundle.py index 85a2b209f6..7d1d372b11 100644 --- a/erpnext/selling/doctype/product_bundle/test_product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/test_product_bundle.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals import frappe test_records = frappe.get_test_records('Product Bundle') -def make_product_bundle(parent, items): +def make_product_bundle(parent, items, qty=None): if frappe.db.exists("Product Bundle", parent): return frappe.get_doc("Product Bundle", parent) @@ -17,7 +17,7 @@ def make_product_bundle(parent, items): }) for item in items: - product_bundle.append("items", {"item_code": item, "qty": 1}) + product_bundle.append("items", {"item_code": item, "qty": qty or 1}) product_bundle.insert() diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 7cfec5a046..449a968a4f 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -99,6 +99,8 @@ class Quotation(SellingController): self.update_lead() def on_cancel(self): + if self.lost_reasons: + self.lost_reasons = [] super(Quotation, self).on_cancel() #update enquiry status @@ -197,9 +199,9 @@ def set_expired_status(): cond = "qo.docstatus = 1 and qo.status != 'Expired' and qo.valid_till < %s" # check if those QUO have SO against it so_against_quo = """ - SELECT + SELECT so.name FROM `tabSales Order` so, `tabSales Order Item` so_item - WHERE + WHERE so_item.docstatus = 1 and so.docstatus = 1 and so_item.parent = so.name and so_item.prevdoc_docname = qo.name""" diff --git a/erpnext/selling/doctype/quotation/quotation_list.js b/erpnext/selling/doctype/quotation/quotation_list.js index 802c0ba641..f425acf180 100644 --- a/erpnext/selling/doctype/quotation/quotation_list.js +++ b/erpnext/selling/doctype/quotation/quotation_list.js @@ -3,13 +3,15 @@ frappe.listview_settings['Quotation'] = { "company", "currency", 'valid_till'], onload: function(listview) { - listview.page.fields_dict.quotation_to.get_query = function() { - return { - "filters": { - "name": ["in", ["Customer", "Lead"]], - } + if (listview.page.fields_dict.quotation_to) { + listview.page.fields_dict.quotation_to.get_query = function() { + return { + "filters": { + "name": ["in", ["Customer", "Lead"]], + } + }; }; - }; + } }, get_indicator: function(doc) { diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index ee6b429cca..b4c3d79f31 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -280,5 +280,3 @@ def make_quotation(**args): qo.submit() return qo - - diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index d50397cfad..59ae7b2323 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -48,6 +48,10 @@ "base_net_amount", "pricing_rules", "is_free_item", + "section_break_43", + "valuation_rate", + "column_break_45", + "gross_profit", "item_weight_details", "weight_per_unit", "total_weight", @@ -602,12 +606,40 @@ "label": "Against Blanket Order", "no_copy": 1, "print_hide": 1 + }, + { + "fieldname": "section_break_43", + "fieldtype": "Section Break" + }, + { + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "label": "Valuation Rate", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1, + "report_hide": 1 + }, + { + "fieldname": "column_break_45", + "fieldtype": "Column Break" + }, + { + "fieldname": "gross_profit", + "fieldtype": "Currency", + "label": "Gross Profit", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1, + "report_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-30 18:40:28.782720", + "modified": "2020-05-19 20:48:43.222229", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 45a43c5e7e..705dcb8e03 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -34,6 +34,15 @@ frappe.ui.form.on("Sales Order", { } }; }) + + frm.set_query("bom_no", "items", function(doc, cdt, cdn) { + var row = locals[cdt][cdn]; + return { + filters: { + "item": row.item_code + } + } + }); }, refresh: function(frm) { if(frm.doc.docstatus === 1 && frm.doc.status !== 'Closed' diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 6462d3bc8c..cd4e1d0792 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -143,11 +143,15 @@ { "fieldname": "customer_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "options": "fa fa-user" }, { "fieldname": "column_break0", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Column Break", "width": "50%" }, @@ -157,6 +161,8 @@ "fieldname": "title", "fieldtype": "Data", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Title", "no_copy": 1, "print_hide": 1 @@ -164,6 +170,8 @@ { "fieldname": "naming_series", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Series", "no_copy": 1, "oldfieldname": "naming_series", @@ -177,6 +185,8 @@ "bold": 1, "fieldname": "customer", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_global_search": 1, "in_standard_filter": 1, "label": "Customer", @@ -192,6 +202,8 @@ "fetch_from": "customer.customer_name", "fieldname": "customer_name", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "in_global_search": 1, "label": "Customer Name", "read_only": 1 @@ -200,6 +212,8 @@ "default": "Sales", "fieldname": "order_type", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Order Type", "oldfieldname": "order_type", "oldfieldtype": "Select", @@ -210,6 +224,8 @@ { "fieldname": "column_break1", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Column Break", "width": "50%" }, @@ -217,6 +233,8 @@ "fieldname": "amended_from", "fieldtype": "Link", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "label": "Amended From", "no_copy": 1, @@ -230,6 +248,8 @@ { "fieldname": "company", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Company", "oldfieldname": "company", @@ -244,6 +264,8 @@ "default": "Today", "fieldname": "transaction_date", "fieldtype": "Date", + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Date", "no_copy": 1, @@ -258,6 +280,8 @@ "depends_on": "eval:!doc.skip_delivery_note", "fieldname": "delivery_date", "fieldtype": "Date", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Delivery Date", "no_copy": 1 @@ -266,6 +290,8 @@ "allow_on_submit": 1, "fieldname": "po_no", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Customer's Purchase Order", "oldfieldname": "po_no", "oldfieldtype": "Data", @@ -276,14 +302,19 @@ "depends_on": "eval:doc.po_no", "fieldname": "po_date", "fieldtype": "Date", + "hide_days": 1, + "hide_seconds": 1, "label": "Customer's Purchase Order Date", "oldfieldname": "po_date", "oldfieldtype": "Date", "width": "100px" }, { + "fetch_from": "customer.tax_id", "fieldname": "tax_id", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Tax Id", "read_only": 1, "width": "100px" @@ -293,6 +324,8 @@ "depends_on": "customer", "fieldname": "contact_info", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Address and Contact", "options": "fa fa-bullhorn" }, @@ -300,6 +333,8 @@ "allow_on_submit": 1, "fieldname": "customer_address", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Customer Address", "options": "Address", "print_hide": 1 @@ -308,12 +343,16 @@ "allow_on_submit": 1, "fieldname": "address_display", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Address", "read_only": 1 }, { "fieldname": "contact_person", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Contact Person", "options": "Contact", "print_hide": 1 @@ -321,6 +360,8 @@ { "fieldname": "contact_display", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "in_global_search": 1, "label": "Contact", "read_only": 1 @@ -328,6 +369,8 @@ { "fieldname": "contact_mobile", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Mobile No", "read_only": 1 }, @@ -335,6 +378,8 @@ "fieldname": "contact_email", "fieldtype": "Data", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Contact Email", "options": "Email", "print_hide": 1, @@ -343,24 +388,32 @@ { "fieldname": "company_address_display", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Company Address", "read_only": 1 }, { "fieldname": "company_address", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Company Address Name", "options": "Address" }, { "fieldname": "col_break46", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "width": "50%" }, { "allow_on_submit": 1, "fieldname": "shipping_address_name", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Shipping Address Name", "options": "Address", "print_hide": 1 @@ -369,6 +422,8 @@ "allow_on_submit": 1, "fieldname": "shipping_address", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Shipping Address", "print_hide": 1, "read_only": 1 @@ -377,6 +432,8 @@ "fieldname": "customer_group", "fieldtype": "Link", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Customer Group", "options": "Customer Group", "print_hide": 1 @@ -384,6 +441,8 @@ { "fieldname": "territory", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Territory", "options": "Territory", "print_hide": 1 @@ -392,6 +451,8 @@ "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Currency and Price List", "options": "fa fa-tag", "print_hide": 1 @@ -399,6 +460,8 @@ { "fieldname": "currency", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Currency", "oldfieldname": "currency", "oldfieldtype": "Select", @@ -411,6 +474,8 @@ "description": "Rate at which customer's currency is converted to company's base currency", "fieldname": "conversion_rate", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Exchange Rate", "oldfieldname": "conversion_rate", "oldfieldtype": "Currency", @@ -422,11 +487,15 @@ { "fieldname": "column_break2", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "width": "50%" }, { "fieldname": "selling_price_list", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Price List", "oldfieldname": "price_list_name", "oldfieldtype": "Select", @@ -438,6 +507,8 @@ { "fieldname": "price_list_currency", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Price List Currency", "options": "Currency", "print_hide": 1, @@ -448,6 +519,8 @@ "description": "Rate at which Price list currency is converted to company's base currency", "fieldname": "plc_conversion_rate", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Price List Exchange Rate", "precision": "9", "print_hide": 1, @@ -457,6 +530,8 @@ "default": "0", "fieldname": "ignore_pricing_rule", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, @@ -464,11 +539,15 @@ }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "set_warehouse", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Set Source Warehouse", "options": "Warehouse", "print_hide": 1 @@ -476,18 +555,24 @@ { "fieldname": "items_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Section Break", "options": "fa fa-shopping-cart" }, { "fieldname": "scan_barcode", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Scan Barcode" }, { "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Items", "oldfieldname": "sales_order_details", "oldfieldtype": "Table", @@ -497,32 +582,44 @@ { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Pricing Rules" }, { "fieldname": "pricing_rules", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", "read_only": 1 }, { "fieldname": "section_break_31", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "column_break_33a", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "total_qty", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Quantity", "read_only": 1 }, { "fieldname": "base_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, @@ -531,6 +628,8 @@ { "fieldname": "base_net_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Net Total (Company Currency)", "oldfieldname": "net_total", "oldfieldtype": "Currency", @@ -541,11 +640,15 @@ }, { "fieldname": "column_break_33", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total", "options": "currency", "read_only": 1 @@ -553,6 +656,8 @@ { "fieldname": "net_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Net Total", "options": "currency", "print_hide": 1, @@ -561,6 +666,8 @@ { "fieldname": "total_net_weight", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Net Weight", "print_hide": 1, "read_only": 1 @@ -568,6 +675,8 @@ { "fieldname": "taxes_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Taxes and Charges", "oldfieldtype": "Section Break", "options": "fa fa-money" @@ -575,17 +684,23 @@ { "fieldname": "tax_category", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Tax Category", "options": "Tax Category", "print_hide": 1 }, { "fieldname": "column_break_38", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "shipping_rule", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Shipping Rule", "oldfieldtype": "Button", "options": "Shipping Rule", @@ -593,11 +708,15 @@ }, { "fieldname": "section_break_40", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "taxes_and_charges", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Taxes and Charges Template", "oldfieldname": "charge", "oldfieldtype": "Link", @@ -607,6 +726,8 @@ { "fieldname": "taxes", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Taxes and Charges", "oldfieldname": "other_charges", "oldfieldtype": "Table", @@ -616,11 +737,15 @@ "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Tax Breakup" }, { "fieldname": "other_charges_calculation", "fieldtype": "Long Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -629,11 +754,15 @@ }, { "fieldname": "section_break_43", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "base_total_taxes_and_charges", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Taxes and Charges (Company Currency)", "oldfieldname": "other_charges_total", "oldfieldtype": "Currency", @@ -644,11 +773,15 @@ }, { "fieldname": "column_break_46", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "total_taxes_and_charges", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, @@ -658,6 +791,8 @@ "fieldname": "loyalty_points_redemption", "fieldtype": "Section Break", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Loyalty Points Redemption", "print_hide": 1 }, @@ -665,6 +800,8 @@ "fieldname": "loyalty_points", "fieldtype": "Int", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Loyalty Points", "read_only": 1 }, @@ -672,6 +809,8 @@ "fieldname": "loyalty_amount", "fieldtype": "Currency", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Loyalty Amount", "print_hide": 1, "read_only": 1 @@ -681,11 +820,15 @@ "collapsible_depends_on": "discount_amount", "fieldname": "section_break_48", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Additional Discount and Coupon Code" }, { "fieldname": "coupon_code", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Coupon Code", "options": "Coupon Code" }, @@ -693,6 +836,8 @@ "default": "Grand Total", "fieldname": "apply_discount_on", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", "print_hide": 1 @@ -700,6 +845,8 @@ { "fieldname": "base_discount_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, @@ -707,17 +854,23 @@ }, { "fieldname": "column_break_50", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Additional Discount Percentage", "print_hide": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Additional Discount Amount", "options": "currency", "print_hide": 1 @@ -725,6 +878,8 @@ { "fieldname": "totals", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Section Break", "options": "fa fa-money", "print_hide": 1 @@ -732,6 +887,8 @@ { "fieldname": "base_grand_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Grand Total (Company Currency)", "oldfieldname": "grand_total", "oldfieldtype": "Currency", @@ -743,6 +900,8 @@ { "fieldname": "base_rounding_adjustment", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Rounding Adjustment (Company Currency)", "no_copy": 1, "options": "Company:company:default_currency", @@ -752,6 +911,8 @@ { "fieldname": "base_rounded_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Rounded Total (Company Currency)", "oldfieldname": "rounded_total", "oldfieldtype": "Currency", @@ -764,6 +925,8 @@ "description": "In Words will be visible once you save the Sales Order.", "fieldname": "base_in_words", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "In Words (Company Currency)", "oldfieldname": "in_words", "oldfieldtype": "Data", @@ -774,6 +937,8 @@ { "fieldname": "column_break3", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Column Break", "print_hide": 1, "width": "50%" @@ -781,6 +946,8 @@ { "fieldname": "grand_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Grand Total", "oldfieldname": "grand_total_export", @@ -792,6 +959,8 @@ { "fieldname": "rounding_adjustment", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Rounding Adjustment", "no_copy": 1, "options": "currency", @@ -802,6 +971,8 @@ "bold": 1, "fieldname": "rounded_total", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Rounded Total", "oldfieldname": "rounded_total_export", "oldfieldtype": "Currency", @@ -812,6 +983,8 @@ { "fieldname": "in_words", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "In Words", "oldfieldname": "in_words_export", "oldfieldtype": "Data", @@ -822,6 +995,8 @@ { "fieldname": "advance_paid", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Advance Paid", "no_copy": 1, "options": "party_account_currency", @@ -833,6 +1008,8 @@ "collapsible_depends_on": "packed_items", "fieldname": "packing_list", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Packing List", "oldfieldtype": "Section Break", "options": "fa fa-suitcase", @@ -841,6 +1018,8 @@ { "fieldname": "packed_items", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Packed Items", "options": "Packed Item", "print_hide": 1, @@ -849,11 +1028,15 @@ { "fieldname": "payment_schedule_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Payment Terms" }, { "fieldname": "payment_terms_template", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Payment Terms Template", "options": "Payment Terms Template", "print_hide": 1 @@ -861,6 +1044,8 @@ { "fieldname": "payment_schedule", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", @@ -871,6 +1056,8 @@ "collapsible_depends_on": "terms", "fieldname": "terms_section_break", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Terms and Conditions", "oldfieldtype": "Section Break", "options": "fa fa-legal" @@ -878,6 +1065,8 @@ { "fieldname": "tc_name", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Terms", "oldfieldname": "tc_name", "oldfieldtype": "Link", @@ -887,6 +1076,8 @@ { "fieldname": "terms", "fieldtype": "Text Editor", + "hide_days": 1, + "hide_seconds": 1, "label": "Terms and Conditions Details", "oldfieldname": "terms", "oldfieldtype": "Text Editor" @@ -896,6 +1087,8 @@ "collapsible_depends_on": "project", "fieldname": "more_info", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text", @@ -904,6 +1097,8 @@ { "fieldname": "inter_company_order_reference", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Inter Company Order Reference", "options": "Purchase Order" }, @@ -911,6 +1106,8 @@ "description": "Track this Sales Order against any Project", "fieldname": "project", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Project", "oldfieldname": "project", "oldfieldtype": "Link", @@ -920,6 +1117,8 @@ "fieldname": "party_account_currency", "fieldtype": "Link", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Party Account Currency", "no_copy": 1, "options": "Currency", @@ -928,11 +1127,15 @@ }, { "fieldname": "column_break_77", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "source", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Source", "oldfieldname": "source", "oldfieldtype": "Select", @@ -942,6 +1145,8 @@ { "fieldname": "campaign", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Campaign", "oldfieldname": "campaign", "oldfieldtype": "Link", @@ -952,11 +1157,15 @@ "collapsible": 1, "fieldname": "printing_details", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Print Settings" }, { "fieldname": "language", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Print Language", "print_hide": 1, "read_only": 1 @@ -965,6 +1174,8 @@ "allow_on_submit": 1, "fieldname": "letter_head", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Letter Head", "oldfieldname": "letter_head", "oldfieldtype": "Select", @@ -974,6 +1185,8 @@ { "fieldname": "column_break4", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "oldfieldtype": "Column Break", "print_hide": 1, "width": "50%" @@ -982,6 +1195,8 @@ "allow_on_submit": 1, "fieldname": "select_print_heading", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Print Heading", "no_copy": 1, "oldfieldname": "select_print_heading", @@ -995,6 +1210,8 @@ "default": "0", "fieldname": "group_same_items", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Group same items", "print_hide": 1 }, @@ -1002,6 +1219,8 @@ "collapsible": 1, "fieldname": "section_break_78", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Billing and Delivery Status", "oldfieldtype": "Column Break", "print_hide": 1, @@ -1011,6 +1230,8 @@ "default": "Draft", "fieldname": "status", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Status", "no_copy": 1, @@ -1027,6 +1248,8 @@ "fieldname": "delivery_status", "fieldtype": "Select", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Delivery Status", "no_copy": 1, @@ -1038,6 +1261,8 @@ "description": "% of materials delivered against this Sales Order", "fieldname": "per_delivered", "fieldtype": "Percent", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "% Delivered", "no_copy": 1, @@ -1049,13 +1274,17 @@ }, { "fieldname": "column_break_81", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "depends_on": "eval:!doc.__islocal", "description": "% of materials billed against this Sales Order", "fieldname": "per_billed", "fieldtype": "Percent", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "% Amount Billed", "no_copy": 1, @@ -1069,6 +1298,8 @@ "fieldname": "billing_status", "fieldtype": "Select", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Billing Status", "no_copy": 1, @@ -1080,6 +1311,8 @@ "collapsible_depends_on": "commission_rate", "fieldname": "sales_team_section_break", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Commission", "oldfieldtype": "Section Break", "options": "fa fa-group", @@ -1088,6 +1321,8 @@ { "fieldname": "sales_partner", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Partner", "oldfieldname": "sales_partner", "oldfieldtype": "Link", @@ -1098,12 +1333,16 @@ { "fieldname": "column_break7", "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1, "print_hide": 1, "width": "50%" }, { "fieldname": "commission_rate", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "label": "Commission Rate", "oldfieldname": "commission_rate", "oldfieldtype": "Currency", @@ -1113,6 +1352,8 @@ { "fieldname": "total_commission", "fieldtype": "Currency", + "hide_days": 1, + "hide_seconds": 1, "label": "Total Commission", "oldfieldname": "total_commission", "oldfieldtype": "Currency", @@ -1124,6 +1365,8 @@ "collapsible_depends_on": "sales_team", "fieldname": "section_break1", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Team", "print_hide": 1 }, @@ -1131,6 +1374,8 @@ "allow_on_submit": 1, "fieldname": "sales_team", "fieldtype": "Table", + "hide_days": 1, + "hide_seconds": 1, "label": "Sales Team", "oldfieldname": "sales_team", "oldfieldtype": "Table", @@ -1139,8 +1384,11 @@ }, { "allow_on_submit": 1, + "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, "label": "Auto Repeat Section", "no_copy": 1, "print_hide": 1, @@ -1150,6 +1398,8 @@ "allow_on_submit": 1, "fieldname": "from_date", "fieldtype": "Date", + "hide_days": 1, + "hide_seconds": 1, "label": "From Date", "no_copy": 1 }, @@ -1157,16 +1407,22 @@ "allow_on_submit": 1, "fieldname": "to_date", "fieldtype": "Date", + "hide_days": 1, + "hide_seconds": 1, "label": "To Date", "no_copy": 1 }, { "fieldname": "column_break_108", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "auto_repeat", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Auto Repeat", "options": "Auto Repeat" }, @@ -1175,11 +1431,15 @@ "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", + "hide_days": 1, + "hide_seconds": 1, "label": "Update Auto Repeat Reference" }, { "fieldname": "contact_phone", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Phone", "read_only": 1 }, @@ -1188,6 +1448,8 @@ "fieldname": "skip_delivery_note", "fieldtype": "Check", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "Skip Delivery Note", "print_hide": 1 } @@ -1196,7 +1458,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-04-17 12:50:39.640534", + "modified": "2020-06-30 11:56:42.301317", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 05e4aa892b..ffb66354fa 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -868,7 +868,8 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe ], "field_no_map": [ "rate", - "price_list_rate" + "price_list_rate", + "item_tax_template" ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.qty and doc.supplier == supplier and doc.item_code in selected_items diff --git a/erpnext/selling/doctype/sales_order/test_records.json b/erpnext/selling/doctype/sales_order/test_records.json index 6cbd6c2fc1..8a090e6d3d 100644 --- a/erpnext/selling/doctype/sales_order/test_records.json +++ b/erpnext/selling/doctype/sales_order/test_records.json @@ -1,39 +1,39 @@ [ { "advance_paid": 0.0, - "company": "_Test Company", - "conversion_rate": 1.0, - "currency": "INR", - "customer": "_Test Customer", - "customer_group": "_Test Customer Group", - "customer_name": "_Test Customer", - "doctype": "Sales Order", - "base_grand_total": 1000.0, - "grand_total": 1000.0, - "naming_series": "_T-Sales Order-", - "order_type": "Sales", - "plc_conversion_rate": 1.0, - "price_list_currency": "INR", + "company": "_Test Company", + "conversion_rate": 1.0, + "currency": "INR", + "customer": "_Test Customer", + "customer_group": "_Test Customer Group", + "customer_name": "_Test Customer", + "doctype": "Sales Order", + "base_grand_total": 1000.0, + "grand_total": 1000.0, + "naming_series": "_T-Sales Order-", + "order_type": "Sales", + "plc_conversion_rate": 1.0, + "price_list_currency": "INR", "items": [ { - "base_amount": 1000.0, - "base_rate": 100.0, - "description": "CPU", - "doctype": "Sales Order Item", - "item_code": "_Test Item Home Desktop 100", - "item_name": "CPU", - "delivery_date": "2013-02-23", - "parentfield": "items", - "qty": 10.0, - "rate": 100.0, + "base_amount": 1000.0, + "base_rate": 100.0, + "description": "CPU", + "doctype": "Sales Order Item", + "item_code": "_Test Item", + "item_name": "_Test Item 1", + "delivery_date": "2013-02-23", + "parentfield": "items", + "qty": 10.0, + "rate": 100.0, "warehouse": "_Test Warehouse - _TC", "stock_uom": "_Test UOM", "conversion_factor": 1.0, "uom": "_Test UOM" } - ], - "selling_price_list": "_Test Price List", - "territory": "_Test Territory", + ], + "selling_price_list": "_Test Price List", + "territory": "_Test Territory", "transaction_date": "2013-02-21" } ] \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index b8b0d404e5..accaa9c3b3 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals import frappe +import json from frappe.utils import flt, add_days, nowdate import frappe.permissions import unittest @@ -10,9 +11,10 @@ from erpnext.selling.doctype.sales_order.sales_order \ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.selling.doctype.sales_order.sales_order import make_work_orders from erpnext.controllers.accounts_controller import update_child_qty_rate -import json from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order +from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle +from erpnext.stock.doctype.item.test_item import make_item class TestSalesOrder(unittest.TestCase): def tearDown(self): @@ -400,6 +402,43 @@ class TestSalesOrder(unittest.TestCase): 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_qty_rate_perm(self): + so = make_sales_order(item_code= "_Test Item", qty=4) + + 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': 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) + frappe.set_user("Administrator") + + def test_update_child_qty_rate_product_bundle(self): + # 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.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) + + 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) + def test_warehouse_user(self): frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com") @@ -440,8 +479,6 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.CancelledLinkError, dn.submit) def test_service_type_product_bundle(self): - from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle - from erpnext.stock.doctype.item.test_item import make_item make_item("_Test Service Product Bundle", {"is_stock_item": 0}) make_item("_Test Service Product Bundle Item 1", {"is_stock_item": 0}) make_item("_Test Service Product Bundle Item 2", {"is_stock_item": 0}) @@ -455,8 +492,6 @@ class TestSalesOrder(unittest.TestCase): self.assertTrue("_Test Service Product Bundle Item 2" in [d.item_code for d in so.packed_items]) def test_mix_type_product_bundle(self): - from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle - from erpnext.stock.doctype.item.test_item import make_item make_item("_Test Mix Product Bundle", {"is_stock_item": 0}) make_item("_Test Mix Product Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Mix Product Bundle Item 2", {"is_stock_item": 0}) @@ -467,7 +502,6 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(WarehouseRequired, make_sales_order, item_code = "_Test Mix Product Bundle", warehouse="") def test_auto_insert_price(self): - from erpnext.stock.doctype.item.test_item import make_item 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) @@ -502,7 +536,6 @@ class TestSalesOrder(unittest.TestCase): from erpnext.buying.doctype.purchase_order.purchase_order import update_status make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - from erpnext.stock.doctype.item.test_item import make_item 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}) @@ -697,7 +730,6 @@ class TestSalesOrder(unittest.TestCase): def test_serial_no_based_delivery(self): frappe.set_value("Stock Settings", None, "automatically_set_serial_nos_based_on_fifo", 1) - from erpnext.stock.doctype.item.test_item import make_item item = make_item("_Reserved_Serialized_Item", {"is_stock_item": 1, "maintain_stock": 1, "has_serial_no": 1, @@ -818,7 +850,6 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.LinkExistsError, so_doc.cancel) def test_request_for_raw_materials(self): - from erpnext.stock.doctype.item.test_item import make_item item = make_item("_Test Finished Item", {"is_stock_item": 1, "maintain_stock": 1, "valuation_rate": 500, 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 73f233c537..eff17f8bc7 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -72,6 +72,8 @@ "against_blanket_order", "blanket_order", "blanket_order_rate", + "manufacturing_section_section", + "bom_no", "planning_section", "projected_qty", "actual_qty", @@ -212,6 +214,7 @@ "fieldtype": "Link", "label": "UOM", "options": "UOM", + "print_hide": 0, "reqd": 1 }, { @@ -764,12 +767,25 @@ "fieldname": "against_blanket_order", "fieldtype": "Check", "label": "Against Blanket Order" + }, + { + "fieldname": "bom_no", + "fieldtype": "Link", + "label": "BOM No", + "no_copy": 1, + "options": "BOM", + "print_hide": 1 + }, + { + "fieldname": "manufacturing_section_section", + "fieldtype": "Section Break", + "label": "Manufacturing Section" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-05 14:20:28.085117", + "modified": "2020-05-29 20:54:32.309460", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.js b/erpnext/selling/doctype/selling_settings/selling_settings.js index cf6fb2806e..95a4243fb4 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.js +++ b/erpnext/selling/doctype/selling_settings/selling_settings.js @@ -6,3 +6,26 @@ frappe.ui.form.on('Selling Settings', { } }); + +frappe.tour['Selling Settings'] = [ + { + fieldname: "cust_master_name", + title: "Customer Naming By", + description: __("By default, the Customer Name is set as per the Full Name entered. If you want Customers to be named by a ") + "Naming Series" + __(" choose the 'Naming Series' option."), + }, + { + fieldname: "selling_price_list", + title: "Default Selling Price List", + description: __("Configure the default Price List when creating a new Sales transaction. Item prices will be fetched from this Price List.") + }, + { + fieldname: "so_required", + title: "Sales Order Required for Sales Invoice & Delivery Note Creation", + description: __("If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice or Delivery Note without creating a Sales Order first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Sales Order' checkbox in the Customer master.") + }, + { + fieldname: "dn_required", + title: "Delivery Note Required for Sales Invoice Creation", + description: __("If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice without creating a Delivery Note first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Delivery Note' checkbox in the Customer master.") + } +]; \ No newline at end of file diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index c04bfd281e..dcbc0748f7 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2013-06-25 10:25:16", "description": "Settings for Selling Module", "doctype": "DocType", @@ -79,13 +80,13 @@ { "fieldname": "so_required", "fieldtype": "Select", - "label": "Sales Order Required", + "label": "Sales Order Required for Sales Invoice & Delivery Note Creation", "options": "No\nYes" }, { "fieldname": "dn_required", "fieldtype": "Select", - "label": "Delivery Note Required", + "label": "Delivery Note Required for Sales Invoice Creation", "options": "No\nYes" }, { @@ -137,7 +138,8 @@ "icon": "fa fa-cog", "idx": 1, "issingle": 1, - "modified": "2019-12-09 13:38:36.486298", + "links": [], + "modified": "2020-06-01 13:58:35.637858", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/module_onboarding/selling/selling.json b/erpnext/selling/module_onboarding/selling/selling.json new file mode 100644 index 0000000000..160208ff68 --- /dev/null +++ b/erpnext/selling/module_onboarding/selling/selling.json @@ -0,0 +1,53 @@ +{ + "allow_roles": [ + { + "role": "Sales Manager" + }, + { + "role": "Sales User" + }, + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + } + ], + "creation": "2020-06-01 12:44:42.589930", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/selling", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-08 14:05:37.669753", + "modified_by": "Administrator", + "module": "Selling", + "name": "Selling", + "owner": "Administrator", + "steps": [ + { + "step": "Introduction to Selling" + }, + { + "step": "Create a Customer" + }, + { + "step": "Setup your Warehouse" + }, + { + "step": "Create a Product" + }, + { + "step": "Create a Quotation" + }, + { + "step": "Create your first Sales Order" + }, + { + "step": "Selling Settings" + } + ], + "subtitle": "Products, Sales, Analysis, and more.", + "success_message": "The Selling Module is all set up!", + "title": "Let's Set Up the Selling Module." +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json new file mode 100644 index 0000000000..5a403b06cf --- /dev/null +++ b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:46:41.831517", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 13:16:19.731719", + "modified_by": "Administrator", + "name": "Create a Customer", + "owner": "Administrator", + "reference_document": "Customer", + "show_full_form": 0, + "title": "Create a Customer", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json new file mode 100644 index 0000000000..d2068e167b --- /dev/null +++ b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 18:16:06.624554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-12 18:30:02.489949", + "modified_by": "Administrator", + "name": "Create a Product", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create a Product", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json new file mode 100644 index 0000000000..27253d15b6 --- /dev/null +++ b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-06-01 13:34:58.958641", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 13:34:58.958641", + "modified_by": "Administrator", + "name": "Create a Quotation", + "owner": "Administrator", + "reference_document": "Quotation", + "show_full_form": 1, + "title": "Create a Quotation", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/create_product/create_product.json b/erpnext/selling/onboarding_step/create_product/create_product.json new file mode 100644 index 0000000000..0ffa30158b --- /dev/null +++ b/erpnext/selling/onboarding_step/create_product/create_product.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-05 16:42:31.476275", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 12:50:59.010439", + "modified_by": "Administrator", + "name": "Create Product", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create a Finished Good", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/create_your_first_sales_order/create_your_first_sales_order.json b/erpnext/selling/onboarding_step/create_your_first_sales_order/create_your_first_sales_order.json new file mode 100644 index 0000000000..5b601a7a90 --- /dev/null +++ b/erpnext/selling/onboarding_step/create_your_first_sales_order/create_your_first_sales_order.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-06-01 12:52:27.181841", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 12:52:27.181841", + "modified_by": "Administrator", + "name": "Create your first Sales Order", + "owner": "Administrator", + "reference_document": "Sales Order", + "show_full_form": 1, + "title": "Create your first Sales Order", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json new file mode 100644 index 0000000000..d21c1f4954 --- /dev/null +++ b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json @@ -0,0 +1,19 @@ +{ + "action": "Watch Video", + "creation": "2020-06-01 12:44:32.089234", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-06-01 13:29:13.703177", + "modified_by": "Administrator", + "name": "Introduction to Selling", + "owner": "Administrator", + "show_full_form": 0, + "title": "Introduction to Selling", + "validate_action": 1, + "video_url": "https://youtu.be/1eP90MWoDQM" +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json new file mode 100644 index 0000000000..7996d7b159 --- /dev/null +++ b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json @@ -0,0 +1,19 @@ +{ + "action": "Show Form Tour", + "creation": "2020-06-01 13:01:45.615189", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 1, + "is_skipped": 0, + "modified": "2020-06-01 13:04:14.980743", + "modified_by": "Administrator", + "name": "Selling Settings", + "owner": "Administrator", + "reference_document": "Selling Settings", + "show_full_form": 0, + "title": "Configure Selling Settings.", + "validate_action": 0 +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json new file mode 100644 index 0000000000..9457deee26 --- /dev/null +++ b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -0,0 +1,20 @@ +{ + "action": "Go to Page", + "creation": "2020-05-19 18:54:19.383397", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-04 12:33:16.970031", + "modified_by": "Administrator", + "name": "Setup your Warehouse", + "owner": "Administrator", + "path": "Tree/Warehouse", + "reference_document": "Warehouse", + "show_full_form": 0, + "title": "Set up your Warehouse", + "validate_action": 1 +} \ No newline at end of file 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 dfa0f7f2db..1ae1fde588 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -167,6 +167,7 @@ def get_item_group_condition(pos_profile): return cond % tuple(item_groups) +@frappe.whitelist() def item_group_query(doctype, txt, searchfield, start, page_len, filters): item_groups = [] cond = "1=1" @@ -187,4 +188,4 @@ def item_group_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() def get_pos_fields(): return frappe.get_all("POS Field", fields=["label", "fieldname", - "fieldtype", "default_value", "reqd", "read_only", "options"]) \ No newline at end of file + "fieldtype", "default_value", "reqd", "read_only", "options"]) diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.js b/erpnext/selling/page/sales_funnel/sales_funnel.js index 85c0cd8bf0..e3d0a55c3a 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.js +++ b/erpnext/selling/page/sales_funnel/sales_funnel.js @@ -90,6 +90,10 @@ erpnext.SalesFunnel = class SalesFunnel { get_data(btn) { var me = this; + if (!this.company) { + frappe.throw(__("Please Select a Company.")); + } + const method_map = { "sales_funnel": "erpnext.selling.page.sales_funnel.sales_funnel.get_funnel_data", "opp_by_lead_source": "erpnext.selling.page.sales_funnel.sales_funnel.get_opp_by_lead_source", diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index d62e2093c6..dba24ef5b0 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -8,14 +8,23 @@ from frappe import _ from erpnext.accounts.report.utils import convert import pandas as pd +def validate_filters(from_date, to_date, company): + if from_date and to_date and (from_date >= to_date): + frappe.throw(_("To Date must be greater than From Date")) + + 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` where (date(`modified`) between %s and %s) and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0] active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact - left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer' + left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer' and (date(contact.modified) between %s and %s) and status != "Passive" """, (from_date, to_date))[0][0] opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` @@ -38,6 +47,8 @@ def get_funnel_data(from_date, to_date, company): @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']) if opportunities: @@ -68,11 +79,13 @@ def get_opp_by_lead_source(from_date, to_date, company): @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']) if opportunities: 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] df = pd.DataFrame(cp_opportunities).groupby(['sales_stage'], as_index=True).agg({'compound_amount': 'sum'}).to_dict() 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 88bd9c135d..f15f63d7bb 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 @@ -5,183 +5,183 @@ from __future__ import unicode_literals import calendar import frappe from frappe import _ -from frappe.utils import cint, cstr +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': _('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': _('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': - return get_data_by_time(filters, common_columns) - else: - return get_data_by_territory(filters, common_columns) + common_columns = [ + { + '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': _('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': _('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': + 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 - }, - ] - columns += common_columns + # key yyyy-mm + columns = [ + { + '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) + 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('-') + # 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, 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) - 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] - }) - return columns, out + 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) + 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] + }) + return columns, out def get_data_by_territory(filters, common_columns): - columns = [{ - 'label': 'Territory', - 'fieldname': 'territory', - 'fieldtype': 'Link', - 'options': 'Territory', - 'width': 150 - }] - columns += common_columns + columns = [{ + 'label': 'Territory', + 'fieldname': 'territory', + 'fieldtype': 'Link', + 'options': 'Territory', + 'width': 150 + }] + columns += common_columns - customers_in = get_customer_stats(filters, tree_view=True) + 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 - } - }) + 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 + } + }) - depth_map = frappe._dict() - for name, info in territory_dict.items(): - default = depth_map.get(info['parent']) + 1 if info['parent'] else 0 - depth_map.setdefault(name, default) + depth_map = frappe._dict() + for name, info in territory_dict.items(): + 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] - 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 - } - data.append(temp) + 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] + 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 + } + 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] - for key in parent_data.keys(): - if key not in ['indent', 'territory', 'parent_territory', 'bold']: - parent_data[key] += ld[key] + for ld in loop_data: + 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']: + parent_data[key] += ld[key] - return columns, data, None, None, None, 1 + return columns, data, None, None, None, 1 def get_customer_stats(filters, tree_view=False): - """ Calculates number of new and repeated customers. """ - 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 = {} + customers = [] + customers_in = {} - 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 and posting_date >= %(from_date)s - {company_condition} order by posting_date'''.format(company_condition=company_condition), - filters, as_dict=1): + 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): - key = si.territory if tree_view else si.posting_date.strftime('%Y-%m') - 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 not si.customer in customers: - customers_in[key]['new'][0] += 1 - customers_in[key]['new'][1] += si.base_grand_total - customers.append(si.customer) - else: - customers_in[key]['repeat'][0] += 1 - customers_in[key]['repeat'][1] += si.base_grand_total + # 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.append(si.customer) - return customers_in + return customers_in diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js index daca2e3bd0..f47d67fe49 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js @@ -12,12 +12,6 @@ frappe.query_reports["Item-wise Sales History"] = { default: frappe.defaults.get_user_default("Company"), reqd: 1 }, - { - fieldname:"item_group", - label: __("Item Group"), - fieldtype: "Link", - options: "Item Group" - }, { fieldname:"from_date", reqd: 1, @@ -32,6 +26,38 @@ frappe.query_reports["Item-wise Sales History"] = { label: __("To Date"), fieldtype: "Date", }, + { + fieldname:"item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group" + }, + { + fieldname:"item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + get_query: () => { + return { + query: "erpnext.controllers.queries.item_query" + } + } + }, + { + fieldname:"customer", + label: __("Customer"), + fieldtype: "Link", + options: "Customer" + } + ], - ] + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + let format_fields = ["delivered_quantity", "billed_amount"]; + + if (in_list(format_fields, column.fieldname) && data && data[column.fieldname] > 0) { + value = "" + value + ""; + } + return value; + } }; \ No newline at end of file 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 405004ece5..1bc4657f29 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 @@ -9,9 +9,15 @@ from frappe.utils.nestedset import get_descendants_of def execute(filters=None): filters = frappe._dict(filters or {}) + if filters.from_date > filters.to_date: + frappe.throw(_('From Date cannot be greater than To Date')) + columns = get_columns(filters) data = get_data(filters) - return columns, data + + chart_data = get_chart_data(data) + + return columns, data, None, chart_data def get_columns(filters): return [ @@ -96,7 +102,7 @@ def get_columns(filters): "label": _("Customer Group"), "fieldtype": "Link", "fieldname": "customer_group", - "options": "customer Group", + "options": "Customer Group", "width": 120 }, { @@ -181,6 +187,12 @@ def get_conditions(filters): 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) + + if filters.get("customer"): + conditions += "AND so.customer = '%s'" %frappe.db.escape(filters.customer) + return conditions def get_customer_details(): @@ -212,3 +224,34 @@ def get_sales_order_details(company_list, filters): AND so.company in ({0}) AND so.docstatus = 1 {1} """.format(','.join(["%s"] * len(company_list)), conditions), tuple(company_list), as_dict=1) + +def get_chart_data(data): + item_wise_sales_map = {} + labels, datapoints = [], [] + + for row in data: + item_key = row.get("item_code") + + if not item_key in item_wise_sales_map: + item_wise_sales_map[item_key] = 0 + + 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))} + + 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] + } + ] + }, + "type" : "bar" + } \ No newline at end of file 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 14d8031582..e89c45182f 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 @@ -158,7 +158,7 @@ def get_data(): } pending_so.append(so_record) else: - for item in bundled_item_map.get((so.name, so.item_code)): + 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')): so_record = { diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py index 67375f98b3..968e2ff26f 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.py +++ b/erpnext/selling/report/quotation_trends/quotation_trends.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +from frappe import _ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): @@ -11,4 +12,48 @@ def execute(filters=None): conditions = get_columns(filters, "Quotation") data = get_data(filters, conditions) - return conditions["columns"], data \ No newline at end of file + chart_data = get_chart_data(data, conditions, filters) + + return conditions["columns"], data, None, chart_data + +def get_chart_data(data, conditions, filters): + if not (data and conditions): + return [] + + datapoints = [] + + start = 2 if filters.get("based_on") in ["Item", "Customer"] else 1 + if filters.get("group_by"): + start += 1 + + # fetch only periodic columns as labels + columns = conditions.get("columns")[start:-2][1::2] + 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]: + 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] + + 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 + } + ] + }, + "type" : "line", + "lineOptions": { + "regionFill": 1 + } + } diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.json b/erpnext/selling/report/sales_analytics/sales_analytics.json index bf9edd6cd4..de5c3a6b2a 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.json +++ b/erpnext/selling/report/sales_analytics/sales_analytics.json @@ -1,5 +1,5 @@ { - "add_total_row": 0, + "add_total_row": 1, "creation": "2018-09-21 12:46:29.451048", "disable_prepared_report": 0, "disabled": 0, @@ -7,7 +7,7 @@ "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2020-04-30 19:49:02.303320", + "modified": "2020-06-19 17:41:03.132101", "modified_by": "Administrator", "module": "Selling", "name": "Sales Analytics", diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 97d9322918..4d113c8e9e 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -23,7 +23,14 @@ class Analytics(object): self.get_columns() self.get_data() self.get_chart_data() - return self.columns, self.data, None, self.chart + + # Skipping total row for tree-view reports + skip_total_row = 0 + + if self.filters.tree_type in ["Supplier Group", "Item Group", "Customer Group", "Territory"]: + skip_total_row = 1 + + return self.columns, self.data, None, self.chart, None, skip_total_row def get_columns(self): self.columns = [{ @@ -194,9 +201,6 @@ class Analytics(object): def get_rows(self): self.data = [] self.get_periodic_data() - total_row = { - "entity": "Total", - } for entity, period_data in iteritems(self.entity_periodic_data): row = { @@ -210,9 +214,6 @@ class Analytics(object): row[scrub(period)] = amount total += amount - if not total_row.get(scrub(period)): total_row[scrub(period)] = 0 - total_row[scrub(period)] += amount - row["total"] = total if self.filters.tree_type == "Item": @@ -220,8 +221,6 @@ class Analytics(object): self.data.append(row) - self.data.append(total_row) - def get_rows_by_group(self): self.get_periodic_data() out = [] diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index 7e8501dcc1..4d81a1e4dd 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -33,21 +33,6 @@ class TestAnalytics(unittest.TestCase): report = execute(filters) expected_data = [ - { - 'entity': 'Total', - 'apr_2017': 0.0, - 'may_2017': 0.0, - 'jun_2017': 2000.0, - 'jul_2017': 1000.0, - 'aug_2017': 0.0, - 'sep_2017': 1500.0, - 'oct_2017': 1000.0, - 'nov_2017': 0.0, - 'dec_2017': 0.0, - 'jan_2018': 0.0, - 'feb_2018': 2000.0, - 'mar_2018': 0.0 - }, { "entity": "_Test Customer 1", "entity_name": "_Test Customer 1", @@ -149,21 +134,6 @@ class TestAnalytics(unittest.TestCase): report = execute(filters) expected_data = [ - { - 'entity': 'Total', - 'apr_2017': 0.0, - 'may_2017': 0.0, - 'jun_2017': 20.0, - 'jul_2017': 10.0, - 'aug_2017': 0.0, - 'sep_2017': 15.0, - 'oct_2017': 10.0, - 'nov_2017': 0.0, - 'dec_2017': 0.0, - 'jan_2018': 0.0, - 'feb_2018': 20.0, - 'mar_2018': 0.0 - }, { "entity": "_Test Customer 1", "entity_name": "_Test Customer 1", diff --git a/erpnext/selling/report/sales_order_analysis/__init__.py b/erpnext/selling/report/sales_order_analysis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js new file mode 100644 index 0000000000..76a5bb51ca --- /dev/null +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js @@ -0,0 +1,85 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Sales Order Analysis"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "width": "80", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_default("company") + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname": "sales_order", + "label": __("Sales Order"), + "fieldtype": "MultiSelectList", + "width": "80", + "options": "Sales Order", + "get_data": function(txt) { + return frappe.db.get_link_options("Sales Order", txt); + }, + "get_query": () =>{ + return { + filters: { "docstatus": 1 } + } + } + }, + { + "fieldname": "status", + "label": __("Status"), + "fieldtype": "MultiSelectList", + "width": "80", + get_data: function(txt) { + let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"] + let options = [] + for (let option of status){ + options.push({ + "value": option, + "description": "" + }) + } + return options + } + }, + { + "fieldname": "group_by_so", + "label": __("Group by Sales Order"), + "fieldtype": "Check", + "default": 0 + } + ], + + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + let format_fields = ["delivered_qty", "billed_amount"]; + + if (in_list(format_fields, column.fieldname) && data && data[column.fieldname] > 0) { + value = "" + value + ""; + } + + if (column.fieldname == "delay" && data && data[column.fieldname] > 0) { + value = "" + value + ""; + } + return value; + } +}; diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.json b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.json new file mode 100644 index 0000000000..c0b1d9aa8c --- /dev/null +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.json @@ -0,0 +1,36 @@ +{ + "add_total_row": 1, + "creation": "2020-05-29 14:54:53.591445", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-05-29 14:54:53.591445", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Order Analysis", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Order", + "report_name": "Sales Order Analysis", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + }, + { + "role": "Maintenance User" + }, + { + "role": "Accounts User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py new file mode 100644 index 0000000000..7e8e6e9e8b --- /dev/null +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -0,0 +1,279 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import copy +from frappe import _ +from frappe.utils import flt, date_diff, getdate + +def execute(filters=None): + if not filters: + return [], [], None, [] + + validate_filters(filters) + + columns = get_columns(filters) + conditions = get_conditions(filters) + data = get_data(conditions, filters) + + if not data: + return [], [], None, [] + + 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") + + if not from_date and to_date: + frappe.throw(_("From and To Dates are required.")) + 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 so.transaction_date between %(from_date)s and %(to_date)s" + + if filters.get("company"): + conditions += " and so.company = %(company)s" + + if filters.get("sales_order"): + conditions += " and so.name in %(sales_order)s" + + if filters.get("status"): + conditions += " and so.status in %(status)s" + + return conditions + +def get_data(conditions, filters): + data = frappe.db.sql(""" + SELECT + so.transaction_date as date, + soi.delivery_date as delivery_date, + so.name as sales_order, + so.status, so.customer, soi.item_code, + DATEDIFF(CURDATE(), soi.delivery_date) as delay_days, + 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, + IFNULL(sii.qty, 0) as billed_qty, + soi.base_amount as amount, + (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, + (soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount, + (soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount, + soi.warehouse as warehouse, + so.company, soi.name + FROM + `tabSales Order` so, + `tabSales Order Item` soi + LEFT JOIN `tabSales Invoice Item` sii + ON sii.so_detail = soi.name + WHERE + soi.parent = so.name + and so.status not in ('Stopped', 'Closed', 'On Hold') + and so.docstatus = 1 + {conditions} + GROUP BY soi.name + ORDER BY so.transaction_date ASC + """.format(conditions=conditions), filters, as_dict=1) + + return data + +def prepare_data(data, filters): + completed, pending = 0, 0 + + if filters.get("group_by_so"): + sales_order_map = {} + + for row in data: + # sum data for chart + completed += row["billed_amount"] + pending += row["pending_amount"] + + # prepare data for report view + row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"]) + + row["delay"] = 0 if row["delay"] < 0 else row["delay"] + if filters.get("group_by_so"): + so_name = row["sales_order"] + + if not so_name in sales_order_map: + # create an entry + row_copy = copy.deepcopy(row) + sales_order_map[so_name] = row_copy + else: + # update existing entry + so_row = sales_order_map[so_name] + so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"])) + 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"] + for field in fields: + so_row[field] = flt(row[field]) + flt(so_row[field]) + + chart_data = prepare_chart_data(pending, completed) + + if filters.get("group_by_so"): + data = [] + for so in sales_order_map: + data.append(sales_order_map[so]) + return data, chart_data + + 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 + } + +def get_columns(filters): + columns = [ + { + "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 + }, + { + "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.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({ + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 100 + }) + + + return columns \ No newline at end of file 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 c0a0f085d9..de7d3f2f77 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +from frappe import _ from erpnext.controllers.trends import get_columns,get_data def execute(filters=None): @@ -10,4 +11,48 @@ def execute(filters=None): data = [] conditions = get_columns(filters, "Sales Order") data = get_data(filters, conditions) - return conditions["columns"], data + chart_data = get_chart_data(data, conditions, filters) + + return conditions["columns"], data, None, chart_data + +def get_chart_data(data, conditions, filters): + if not (data and conditions): + return [] + + datapoints = [] + + start = 2 if filters.get("based_on") in ["Item", "Customer"] else 1 + if filters.get("group_by"): + start += 1 + + # fetch only periodic columns as labels + columns = conditions.get("columns")[start:-2][1::2] + 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]: + 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] + + 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 + } + ] + }, + "type" : "line", + "lineOptions": { + "regionFill": 1 + } + } diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 0fbe49eab7..7ae5385a23 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -107,6 +107,9 @@ frappe.ui.form.on("Company", { erpnext.company.set_chart_of_accounts_options(frm.doc); + if (!frappe.user.has_role('System Manager')) { + frm.get_field("delete_company_transactions").hide(); + } }, make_default_tax_template: function(frm) { @@ -134,7 +137,7 @@ frappe.ui.form.on("Company", { var d = frappe.prompt({ fieldtype:"Data", fieldname: "company_name", - label: __("Please re-type company name to confirm"), + label: __("Please enter the company name to confirm"), reqd: 1, description: __("Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.") }, @@ -261,7 +264,10 @@ erpnext.company.setup_queries = function(frm) { ["expenses_included_in_valuation", {"root_type": "Expense", "account_type": "Expenses Included in Valuation"}], ["stock_received_but_not_billed", - {"root_type": "Liability", "account_type": "Stock Received But Not Billed"}] + {"root_type": "Liability", "account_type": "Stock Received But Not Billed"}], + ["service_received_but_not_billed", + {"root_type": "Liability", "account_type": "Service Received But Not Billed"}], + ], function(i, v) { erpnext.company.set_custom_query(frm, v); }); diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 020a93ff6a..221044df3a 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -22,7 +22,6 @@ "default_letter_head", "default_holiday_list", "default_finance_book", - "standard_working_hours", "default_selling_terms", "default_buying_terms", "default_warehouse_for_sales_return", @@ -67,10 +66,12 @@ "payment_terms", "auto_accounting_for_stock_settings", "enable_perpetual_inventory", + "enable_perpetual_inventory_for_non_stock_items", "default_inventory_account", "stock_adjustment_account", "column_break_32", "stock_received_but_not_billed", + "service_received_but_not_billed", "expenses_included_in_valuation", "fixed_asset_depreciation_settings", "accumulated_depreciation_account", @@ -238,11 +239,6 @@ "label": "Default Holiday List", "options": "Holiday List" }, - { - "fieldname": "standard_working_hours", - "fieldtype": "Float", - "label": "Standard Working Hours" - }, { "fieldname": "default_warehouse_for_sales_return", "fieldtype": "Link", @@ -723,6 +719,20 @@ "fieldtype": "Link", "label": "Default Buying Terms", "options": "Terms and Conditions" + }, + { + "fieldname": "service_received_but_not_billed", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Service Received But Not Billed", + "no_copy": 1, + "options": "Account" + }, + { + "default": "0", + "fieldname": "enable_perpetual_inventory_for_non_stock_items", + "fieldtype": "Check", + "label": "Enable Perpetual Inventory For Non Stock Items" } ], "icon": "fa fa-building", @@ -730,7 +740,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2020-03-21 18:09:53.534211", + "modified": "2020-06-24 12:45:31.462195", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 8bcaa28707..47b41a97ad 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -46,6 +46,7 @@ class Company(NestedSet): self.validate_currency() self.validate_coa_input() self.validate_perpetual_inventory() + self.validate_perpetual_inventory_for_non_stock_items() self.check_country_change() self.set_chart_of_accounts() self.validate_parent_company() @@ -182,6 +183,12 @@ class Company(NestedSet): frappe.msgprint(_("Set default inventory account for perpetual inventory"), alert=True, indicator='orange') + def validate_perpetual_inventory_for_non_stock_items(self): + if not self.get("__islocal"): + if cint(self.enable_perpetual_inventory_for_non_stock_items) == 1 and not self.service_received_but_not_billed: + frappe.throw(_("Set default {0} account for perpetual inventory for non stock items").format( + frappe.bold('Service Received But Not Billed'))) + def check_country_change(self): frappe.flags.country_change = False diff --git a/erpnext/setup/doctype/customer_group/customer_group.js b/erpnext/setup/doctype/customer_group/customer_group.js index c199a8e57f..44a5019120 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.js +++ b/erpnext/setup/doctype/customer_group/customer_group.js @@ -8,7 +8,7 @@ cur_frm.cscript.refresh = function(doc, cdt, cdn) { cur_frm.cscript.set_root_readonly = function(doc) { // read-only for root customer group - if(!doc.parent_customer_group) { + if(!doc.parent_customer_group && !doc.__islocal) { cur_frm.set_read_only(); cur_frm.set_intro(__("This is a root customer group and cannot be edited.")); } else { @@ -20,7 +20,8 @@ cur_frm.cscript.set_root_readonly = function(doc) { cur_frm.fields_dict['parent_customer_group'].get_query = function(doc,cdt,cdn) { return { filters: { - 'is_group': 1 + 'is_group': 1, + 'name': ['!=', cur_frm.doc.customer_group_name] } } } diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py index f62613ea1b..68e1ccb635 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.py +++ b/erpnext/setup/doctype/customer_group/customer_group.py @@ -6,9 +6,12 @@ import frappe from frappe import _ -from frappe.utils.nestedset import NestedSet +from frappe.utils.nestedset import NestedSet, get_root_of class CustomerGroup(NestedSet): nsm_parent_field = 'parent_customer_group' + def validate(self): + if not self.parent_customer_group: + self.parent_customer_group = get_root_of("Customer Group") def on_update(self): self.validate_name_with_customer() diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 4d2d540bbc..b30bd7814b 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -101,8 +101,7 @@ class EmailDigest(Document): if not context.purchase_order_list: frappe.throw(_("No items to be received are overdue")) - if not (context.events or context.todo_list or context.notifications or context.cards - or context.purchase_orders_items_overdue_list): + if not context: return None frappe.flags.ignore_account_permission = False @@ -406,8 +405,8 @@ class EmailDigest(Document): 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 status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date})[0] + 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] label = get_link_to_report('Sales Order', label=self.meta.get_label("sales_orders_to_bill"), report_type="Report Builder", @@ -431,8 +430,8 @@ class EmailDigest(Document): 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 status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date})[0] + 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] label = get_link_to_report('Sales Order', label=self.meta.get_label("sales_orders_to_deliver"), report_type="Report Builder", @@ -456,8 +455,8 @@ class EmailDigest(Document): 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 status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date})[0] + 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] label = get_link_to_report('Purchase Order', label=self.meta.get_label("purchase_orders_to_receive"), report_type="Report Builder", @@ -481,8 +480,8 @@ class EmailDigest(Document): 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 status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date})[0] + 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] label = get_link_to_report('Purchase Order', label=self.meta.get_label("purchase_orders_to_bill"), report_type="Report Builder", diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js index df2223192b..9892dc3dcc 100644 --- a/erpnext/setup/doctype/item_group/item_group.js +++ b/erpnext/setup/doctype/item_group/item_group.js @@ -66,7 +66,7 @@ frappe.ui.form.on("Item Group", { set_root_readonly: function(frm) { // read-only for root item group frm.set_intro(""); - if(!frm.doc.parent_item_group) { + if(!frm.doc.parent_item_group && !frm.doc.__islocal) { frm.set_read_only(); frm.set_intro(__("This is a root item group and cannot be edited."), true); } diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js index 89ca4a9dd7..8f7593d6ee 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.js +++ b/erpnext/setup/doctype/sales_person/sales_person.js @@ -19,7 +19,7 @@ frappe.ui.form.on('Sales Person', { } } }; - + frm.make_methods = { 'Sales Order': () => frappe.new_doc("Sales Order") .then(() => frm.add_child("sales_team", {"sales_person": frm.doc.name})) @@ -33,7 +33,7 @@ cur_frm.cscript.refresh = function(doc, cdt, cdn) { cur_frm.cscript.set_root_readonly = function(doc) { // read-only for root - if(!doc.parent_sales_person) { + if(!doc.parent_sales_person && !doc.__islocal) { cur_frm.set_read_only(); cur_frm.set_intro(__("This is a root sales person and cannot be edited.")); } else { diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index 3379534cf8..19c2e5b954 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -5,13 +5,16 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.utils import flt -from frappe.utils.nestedset import NestedSet +from frappe.utils.nestedset import NestedSet, get_root_of from erpnext import get_default_currency class SalesPerson(NestedSet): 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 []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory.")) diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.js b/erpnext/setup/doctype/supplier_group/supplier_group.js index ac5bda6e2c..e75030d441 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.js +++ b/erpnext/setup/doctype/supplier_group/supplier_group.js @@ -8,7 +8,7 @@ cur_frm.cscript.refresh = function(doc) { cur_frm.cscript.set_root_readonly = function(doc) { // read-only for root customer group - if(!doc.parent_supplier_group) { + if(!doc.parent_supplier_group && !doc.__islocal) { cur_frm.set_read_only(); cur_frm.set_intro(__("This is a root supplier group and cannot be edited.")); } else { @@ -20,7 +20,8 @@ cur_frm.cscript.set_root_readonly = function(doc) { cur_frm.fields_dict['parent_supplier_group'].get_query = function() { return { filters: { - 'is_group': 1 + 'is_group': 1, + 'name': ['!=', cur_frm.doc.supplier_group_name] } }; }; diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json index aba6a791a4..28d1d16a05 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "field:title", @@ -49,7 +50,7 @@ "fieldname": "terms_and_conditions_help", "fieldtype": "HTML", "label": "Terms and Conditions Help", - "options": "

    Standard Terms and Conditions Example

    \n\n
    Delivery Terms for Order number {{ name }}\n\n-Order Date : {{ transaction_date }} \n-Expected Delivery Date : {{ delivery_date }}\n
    \n\n

    How to get fieldnames

    \n\n

    The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

    \n\n

    Templating

    \n\n

    Templates are compiled using the Jinja Templating Langauge. To learn more about Jinja, read this documentation.

    " + "options": "

    Standard Terms and Conditions Example

    \n\n
    Delivery Terms for Order number {{ name }}\n\n-Order Date : {{ transaction_date }} \n-Expected Delivery Date : {{ delivery_date }}\n
    \n\n

    How to get fieldnames

    \n\n

    The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

    \n\n

    Templating

    \n\n

    Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

    " }, { "fieldname": "applicable_modules_section", @@ -81,7 +82,8 @@ ], "icon": "icon-legal", "idx": 1, - "modified": "2019-07-04 13:31:30.393425", + "links": [], + "modified": "2020-06-16 22:54:38.094844", "modified_by": "Administrator", "module": "Setup", "name": "Terms and Conditions", diff --git a/erpnext/setup/doctype/territory/territory.js b/erpnext/setup/doctype/territory/territory.js index 1eb9958ce7..ceec47ae8c 100644 --- a/erpnext/setup/doctype/territory/territory.js +++ b/erpnext/setup/doctype/territory/territory.js @@ -20,7 +20,7 @@ cur_frm.cscript.refresh = function(doc, cdt, cdn) { cur_frm.cscript.set_root_readonly = function(doc) { // read-only for root territory - if(!doc.parent_territory) { + if(!doc.parent_territory && !doc.__islocal) { cur_frm.set_read_only(); cur_frm.set_intro(__("This is a root territory and cannot be edited.")); } else { diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 808b5386ab..05e8f666cf 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -6,12 +6,14 @@ import frappe from frappe.utils import flt from frappe import _ -from frappe.utils.nestedset import NestedSet +from frappe.utils.nestedset import NestedSet, get_root_of class Territory(NestedSet): 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 []: if not flt(d.target_qty) and not flt(d.target_amount): diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index e666a41f30..aa9fbc0a92 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -29,12 +29,10 @@ def after_install(): def check_setup_wizard_not_completed(): - if frappe.db.get_default('desktop:home_page') == 'desktop': - print() - print("ERPNext can only be installed on a fresh site where the setup wizard is not completed") - print("You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall") - print() - return False + if frappe.db.get_default('desktop:home_page') != 'setup-wizard': + message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. +You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall""" + frappe.throw(message) def set_single_defaults(): diff --git a/erpnext/setup/setup_wizard/data/dashboard_charts.py b/erpnext/setup/setup_wizard/data/dashboard_charts.py new file mode 100644 index 0000000000..9ce64eb9d9 --- /dev/null +++ b/erpnext/setup/setup_wizard/data/dashboard_charts.py @@ -0,0 +1,151 @@ +from __future__ import unicode_literals +from frappe import _ +import frappe +import json + +def get_company_for_dashboards(): + company = frappe.defaults.get_defaults().company + if company: + return company + else: + company_list = frappe.get_list("Company") + if company_list: + return company_list[0].name + return None + +def get_default_dashboards(): + company = frappe.get_doc("Company", get_company_for_dashboards()) + income_account = company.default_income_account or get_account("Income Account", company.name) + expense_account = company.default_expense_account or get_account("Expense Account", company.name) + bank_account = company.default_bank_account or get_account("Bank", company.name) + + return { + "Dashboards": [ + { + "doctype": "Dashboard", + "dashboard_name": "Accounts", + "charts": [ + { "chart": "Outgoing Bills (Sales Invoice)" }, + { "chart": "Incoming Bills (Purchase Invoice)" }, + { "chart": "Bank Balance" }, + { "chart": "Income" }, + { "chart": "Expenses" }, + { "chart": "Patient Appointments" } + ] + }, + { + "doctype": "Dashboard", + "dashboard_name": "Project", + "charts": [ + { "chart": "Project Summary", "width": "Full" } + ] + }, + ], + "Charts": [ + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "chart_name": "Income", + "timespan": "Last Year", + "color": None, + "filters_json": json.dumps({"company": company.name, "account": income_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line", + "width": "Half" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "chart_name": "Expenses", + "timespan": "Last Year", + "color": None, + "filters_json": json.dumps({"company": company.name, "account": expense_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line", + "width": "Half" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "chart_name": "Bank Balance", + "timespan": "Last Year", + "color": "#ffb868", + "filters_json": json.dumps({"company": company.name, "account": bank_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line", + "width": "Half" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Monthly", + "chart_name": "Incoming Bills (Purchase Invoice)", + "timespan": "Last Year", + "color": "#a83333", + "value_based_on": "base_grand_total", + "filters_json": json.dumps({}), + "chart_type": "Sum", + "timeseries": 1, + "based_on": "posting_date", + "owner": "Administrator", + "document_type": "Purchase Invoice", + "type": "Bar", + "width": "Half" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Monthly", + "chart_name": "Outgoing Bills (Sales Invoice)", + "timespan": "Last Year", + "color": "#7b933d", + "value_based_on": "base_grand_total", + "filters_json": json.dumps({}), + "chart_type": "Sum", + "timeseries": 1, + "based_on": "posting_date", + "owner": "Administrator", + "document_type": "Sales Invoice", + "type": "Bar", + "width": "Half" + }, + { + 'doctype': 'Dashboard Chart', + 'name': 'Project Summary', + 'chart_name': 'Project Summary', + 'chart_type': 'Report', + 'report_name': 'Project Summary', + 'is_public': 1, + 'filters_json': json.dumps({"company": company.name, "status": "Open"}), + 'type': 'Bar', + 'custom_options': '{"type": "bar", "colors": ["#fc4f51", "#78d6ff", "#7575ff"], "axisOptions": { "shortenYAxisNumbers": 1}, "barOptions": { "stacked": 1 }}', + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Daily", + "chart_name": "Patient Appointments", + "timespan": "Last Month", + "color": "#77ecca", + "filters_json": json.dumps({}), + "chart_type": "Count", + "timeseries": 1, + "based_on": "appointment_datetime", + "owner": "Administrator", + "document_type": "Patient Appointment", + "type": "Line", + "width": "Half" + } + ] + } + +def get_account(account_type, company): + accounts = frappe.get_list("Account", filters={"account_type": account_type, "company": company}) + if accounts: + return accounts[0].name diff --git a/erpnext/setup/setup_wizard/data/uom_conversion_data.json b/erpnext/setup/setup_wizard/data/uom_conversion_data.json index 174ecd5903..27a917d9db 100644 --- a/erpnext/setup/setup_wizard/data/uom_conversion_data.json +++ b/erpnext/setup/setup_wizard/data/uom_conversion_data.json @@ -1571,5 +1571,19 @@ "to_uom": "Parts Per Million", "abbr": "ppm", "value": "10000" + }, + { + "category": "Mass", + "from_uom": "Pound", + "to_uom": "Ounce", + "abbr": "oz", + "value": "16" + }, + { + "category": "Mass", + "from_uom": "Gram", + "to_uom": "Ounce", + "abbr": "oz", + "value": "0.035274" } ] \ No newline at end of file diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 8bb0a0529d..ad063cfc9d 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -50,7 +50,7 @@ def install(country=None): 'is_group': 0, 'parent_item_group': _('All Item Groups') }, # salary component - {'doctype': 'Salary Component', 'salary_component': _('Income Tax'), 'description': _('Income Tax'), 'type': 'Deduction'}, + {'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'}, @@ -336,13 +336,14 @@ def add_uom_data(): "category_name": _(d.get("category")) }).insert(ignore_permissions=True) - 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 = [ diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 4ac546e82c..a7e8388be9 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -42,9 +42,9 @@ def get_cart_quotation(doc=None): return { "doc": decorate_quotation_doc(doc), - "shipping_addresses": [{"name": address.name, "display": address.display} + "shipping_addresses": [{"name": address.name, "title": address.address_title, "display": address.display} for address in addresses if address.address_type == "Shipping"], - "billing_addresses": [{"name": address.name, "display": address.display} + "billing_addresses": [{"name": address.name, "title": address.address_title, "display": address.display} for address in addresses if address.address_type == "Billing"], "shipping_rules": get_applicable_shipping_rules(party), "cart_settings": frappe.get_cached_doc("Shopping Cart Settings") @@ -78,8 +78,10 @@ def place_order(): if is_stock_item: item_stock = get_qty_in_stock(item.item_code, "website_warehouse") + if not cint(item_stock.in_stock): + throw(_("{1} Not in Stock").format(item.item_code)) if item.qty > item_stock.stock_qty[0][0]: - throw(_("Only {0} in stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code)) + throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code)) sales_order.flags.ignore_permissions = True sales_order.insert() @@ -319,7 +321,7 @@ def apply_cart_settings(party=None, quotation=None): def set_price_list_and_rate(quotation, cart_settings): """set price list based on billing territory""" - _set_price_list(quotation, cart_settings) + _set_price_list(cart_settings, quotation) # reset values quotation.price_list_currency = quotation.currency = \ @@ -334,23 +336,24 @@ 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(quotation, cart_settings): +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 - - # check if customer price list exists + party_name = quotation.get("party_name") if quotation else get_party().get("name") selling_price_list = None - if quotation.party_name: - selling_price_list = frappe.db.get_value('Customer', quotation.party_name, 'default_price_list') - # else check for territory based price list + # check if default customer price list exists + if party_name: + selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name)) + + # check default price list in shopping cart if not selling_price_list: selling_price_list = cart_settings.price_list - if not selling_price_list and quotation.party_name: - selling_price_list = get_default_price_list(frappe.get_doc("Customer", quotation.party_name)) + if quotation: + quotation.selling_price_list = selling_price_list - quotation.selling_price_list = selling_price_list + return selling_price_list def set_taxes(quotation, cart_settings): """set taxes based on billing territory""" diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js index e1510f5335..ffc5daba62 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js @@ -13,6 +13,12 @@ $.extend(cur_frm.cscript, { }, enable_checkout: function(){ toggle_mandatory(cur_frm) + }, + enabled: function() { + if (cur_frm.doc.enabled === 1) { + cur_frm.doc.show_configure_button = 1; + cur_frm.refresh_field('show_configure_button'); + } } }); diff --git a/erpnext/shopping_cart/product_info.py b/erpnext/shopping_cart/product_info.py index 21ee335125..29617a8748 100644 --- a/erpnext/shopping_cart/product_info.py +++ b/erpnext/shopping_cart/product_info.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from erpnext.shopping_cart.cart import _get_cart_quotation +from erpnext.shopping_cart.cart import _get_cart_quotation, _set_price_list from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings \ import get_shopping_cart_settings, show_quantity_in_website from erpnext.utilities.product import get_price, get_qty_in_stock, get_non_stock_item_status @@ -21,9 +21,11 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False): 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) + price = get_price( item_code, - cart_quotation.selling_price_list, + selling_price_list, cart_settings.default_customer_group, cart_settings.company ) @@ -42,7 +44,7 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False): if product_info["price"]: if frappe.session.user != "Guest": - item = cart_quotation.get({"item_code": item_code}) + item = cart_quotation.get({"item_code": item_code}) if cart_quotation else None if item: product_info["qty"] = item[0].qty @@ -53,7 +55,7 @@ def get_product_info_for_website(item_code, skip_quotation_creation=False): 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) + 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/startup/boot.py b/erpnext/startup/boot.py index 4ca43a89b8..2b80fb8dfa 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -10,7 +10,6 @@ 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.website_settings = frappe.get_doc('Website Settings') if frappe.session['user']!='Guest': update_page_info(bootinfo) diff --git a/erpnext/startup/filters.py b/erpnext/startup/filters.py new file mode 100644 index 0000000000..a99e49b491 --- /dev/null +++ b/erpnext/startup/filters.py @@ -0,0 +1,14 @@ + +import frappe + +def get_filters_config(): + filters_config = { + "fiscal year": { + "label": "Fiscal Year", + "get_field": "erpnext.accounts.utils.get_fiscal_year_filter_field", + "valid_for_fieldtypes": ["Date", "Datetime", "DateRange"], + "depends_on": "company", + } + } + + return filters_config \ No newline at end of file diff --git a/erpnext/stock/dashboard_chart_source/__init__.py b/erpnext/stock/dashboard_chart_source/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/__init__.py b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js new file mode 100644 index 0000000000..a4137547f7 --- /dev/null +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.js @@ -0,0 +1,14 @@ +frappe.provide('frappe.dashboards.chart_sources'); + +frappe.dashboards.chart_sources["Warehouse wise Stock Value"] = { + method: "erpnext.stock.dashboard_chart_source.warehouse_wise_stock_value.warehouse_wise_stock_value.get", + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company") + } + ] +}; \ No newline at end of file diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.json b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.json new file mode 100644 index 0000000000..6d967c0fc0 --- /dev/null +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.json @@ -0,0 +1,13 @@ +{ + "creation": "2020-05-14 14:27:44.108017", + "docstatus": 0, + "doctype": "Dashboard Chart Source", + "idx": 0, + "modified": "2020-05-14 14:27:44.108017", + "modified_by": "Administrator", + "module": "Stock", + "name": "Warehouse wise Stock Value", + "owner": "Administrator", + "source_name": "Warehouse wise Stock Value ", + "timeseries": 0 +} \ No newline at end of file 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 new file mode 100644 index 0000000000..374a34ea7c --- /dev/null +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py @@ -0,0 +1,48 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe, json +from frappe import _ +from frappe.utils.dashboard import cache_source +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): + labels, datapoints = [], [] + filters = frappe.parse_json(filters) + + warehouse_filters = [['is_group', '=', 0]] + if filters and filters.get("company"): + warehouse_filters.append(['company', '=', filters.get("company")]) + + 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)] + + if not warehouses: + return [] + + 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] + + for warehouse in sorted_warehouse_map: + labels.append(_(warehouse.get("name"))) + datapoints.append(warehouse.get("balance")) + + return{ + "labels": labels, + "datasets": [{ + "name": _("Stock Value"), + "values": datapoints + }], + "type": "bar" + } \ No newline at end of file diff --git a/erpnext/stock/dashboard_fixtures.py b/erpnext/stock/dashboard_fixtures.py new file mode 100644 index 0000000000..7625b1ad28 --- /dev/null +++ b/erpnext/stock/dashboard_fixtures.py @@ -0,0 +1,170 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import json +from frappe import _ +from frappe.utils import nowdate +from erpnext.accounts.dashboard_fixtures import _get_fiscal_year +from erpnext.buying.dashboard_fixtures import get_company_for_dashboards + +def get_data(): + fiscal_year = _get_fiscal_year(nowdate()) + + if not fiscal_year: + return frappe._dict() + + company = frappe.get_doc("Company", get_company_for_dashboards()) + fiscal_year_name = fiscal_year.get("name") + start_date = str(fiscal_year.get("year_start_date")) + end_date = str(fiscal_year.get("year_end_date")) + + return frappe._dict({ + "dashboards": get_dashboards(), + "charts": get_charts(company, fiscal_year_name, start_date, end_date), + "number_cards": get_number_cards(company, fiscal_year_name, start_date, end_date), + }) + +def get_dashboards(): + return [{ + "name": "Stock", + "dashboard_name": "Stock", + "charts": [ + { "chart": "Warehouse wise Stock Value", "width": "Full"}, + { "chart": "Purchase Receipt Trends", "width": "Half"}, + { "chart": "Delivery Trends", "width": "Half"}, + { "chart": "Oldest Items", "width": "Half"}, + { "chart": "Item Shortage Summary", "width": "Half"} + ], + "cards": [ + { "card": "Total Active Items"}, + { "card": "Total Warehouses"}, + { "card": "Total Stock Value"} + ] + }] + +def get_charts(company, fiscal_year_name, start_date, end_date): + return [ + { + "doctype": "Dashboard Chart", + "name": "Purchase Receipt Trends", + "time_interval": "Monthly", + "chart_name": _("Purchase Receipt Trends"), + "timespan": "Last Year", + "color": "#7b933d", + "value_based_on": "base_net_total", + "filters_json": json.dumps([["Purchase Receipt", "docstatus", "=", 1]]), + "chart_type": "Sum", + "timeseries": 1, + "based_on": "posting_date", + "owner": "Administrator", + "document_type": "Purchase Receipt", + "type": "Bar", + "width": "Half", + "is_public": 1 + }, + { + "doctype": "Dashboard Chart", + "name": "Delivery Trends", + "time_interval": "Monthly", + "chart_name": _("Delivery Trends"), + "timespan": "Last Year", + "color": "#7b933d", + "value_based_on": "base_net_total", + "filters_json": json.dumps([["Delivery Note", "docstatus", "=", 1]]), + "chart_type": "Sum", + "timeseries": 1, + "based_on": "posting_date", + "owner": "Administrator", + "document_type": "Delivery Note", + "type": "Bar", + "width": "Half", + "is_public": 1 + }, + { + "name": "Warehouse wise Stock Value", + "chart_name": _("Warehouse wise Stock Value"), + "chart_type": "Custom", + "doctype": "Dashboard Chart", + "filters_json": json.dumps({}), + "is_custom": 0, + "is_public": 1, + "owner": "Administrator", + "source": "Warehouse wise Stock Value", + "type": "Bar" + }, + { + "name": "Oldest Items", + "chart_name": _("Oldest Items"), + "chart_type": "Report", + "custom_options": json.dumps({ + "colors": ["#5e64ff"] + }), + "doctype": "Dashboard Chart", + "filters_json": json.dumps({ + "company": company.name, + "to_date": nowdate(), + "show_warehouse_wise_stock": 0 + }), + "is_custom": 1, + "is_public": 1, + "owner": "Administrator", + "report_name": "Stock Ageing", + "type": "Bar" + }, + { + "name": "Item Shortage Summary", + "chart_name": _("Item Shortage Summary"), + "chart_type": "Report", + "doctype": "Dashboard Chart", + "filters_json": json.dumps({ + "company": company.name + }), + "is_custom": 1, + "is_public": 1, + "owner": "Administrator", + "report_name": "Item Shortage Report", + "type": "Bar" + } + ] + +def get_number_cards(company, fiscal_year_name, start_date, end_date): + return [ + { + "name": "Total Active Items", + "label": _("Total Active Items"), + "function": "Count", + "doctype": "Number Card", + "document_type": "Item", + "filters_json": json.dumps([["Item", "disabled", "=", 0]]), + "is_public": 1, + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "name": "Total Warehouses", + "label": _("Total Warehouses"), + "function": "Count", + "doctype": "Number Card", + "document_type": "Warehouse", + "filters_json": json.dumps([["Warehouse", "disabled", "=", 0]]), + "is_public": 1, + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly" + }, + { + "name": "Total Stock Value", + "label": _("Total Stock Value"), + "function": "Sum", + "aggregate_function_based_on": "stock_value", + "doctype": "Number Card", + "document_type": "Bin", + "filters_json": json.dumps([]), + "is_public": 1, + "owner": "Administrator", + "show_percentage_stats": 1, + "stats_time_interval": "Daily" + } + ] \ No newline at end of file diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 38475a6f26..1bf81f7f0e 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -1,9 +1,14 @@ { "cards": [ + { + "hidden": 0, + "label": "Items and Pricing", + "links": "[\n {\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Item Group\",\n \"link\": \"Tree/Item Group\",\n \"name\": \"Item Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Product Bundle\",\n \"name\": \"Product Bundle\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Price List\",\n \"name\": \"Price List\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Price\",\n \"name\": \"Item Price\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipping Rule\",\n \"name\": \"Shipping Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Pricing Rule\",\n \"name\": \"Pricing Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Alternative\",\n \"name\": \"Item Alternative\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Manufacturer\",\n \"name\": \"Item Manufacturer\",\n \"type\": \"doctype\"\n }\n]" + }, { "hidden": 0, "label": "Stock Transactions", - "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -13,12 +18,7 @@ { "hidden": 0, "label": "Settings", - "links": "[\n {\n \"label\": \"Stock Settings\",\n \"name\": \"Stock Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Warehouse\",\n \"name\": \"Warehouse\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Unit of Measure (UOM)\",\n \"name\": \"UOM\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Brand\",\n \"name\": \"Brand\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Attribute\",\n \"name\": \"Item Attribute\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Variant Settings\",\n \"name\": \"Item Variant Settings\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Items and Pricing", - "links": "[\n {\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Product Bundle\",\n \"name\": \"Product Bundle\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Item Group\",\n \"link\": \"Tree/Item Group\",\n \"name\": \"Item Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Price List\",\n \"name\": \"Price List\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Price\",\n \"name\": \"Item Price\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipping Rule\",\n \"name\": \"Shipping Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Pricing Rule\",\n \"name\": \"Pricing Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Alternative\",\n \"name\": \"Item Alternative\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Manufacturer\",\n \"name\": \"Item Manufacturer\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Variant Settings\",\n \"name\": \"Item Variant Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Stock Settings\",\n \"name\": \"Stock Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Warehouse\",\n \"name\": \"Warehouse\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Unit of Measure (UOM)\",\n \"name\": \"UOM\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Variant Settings\",\n \"name\": \"Item Variant Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Brand\",\n \"name\": \"Brand\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Attribute\",\n \"name\": \"Item Attribute\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -33,7 +33,7 @@ { "hidden": 0, "label": "Key Reports", - "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Ordered Items To Be Delivered\",\n \"name\": \"Ordered Items To Be Delivered\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Items To Be Received\",\n \"name\": \"Purchase Order Items To Be Received\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -41,34 +41,46 @@ "links": "[\n {\n \"dependencies\": [\n \"Material Request\"\n ],\n \"doctype\": \"Material Request\",\n \"is_query_report\": true,\n \"label\": \"Requested Items To Be Transferred\",\n \"name\": \"Requested Items To Be Transferred\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Ledger Entry\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Batch Item Expiry Status\",\n \"name\": \"Batch Item Expiry Status\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Price List\"\n ],\n \"doctype\": \"Price List\",\n \"is_query_report\": true,\n \"label\": \"Item Prices\",\n \"name\": \"Item Prices\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Itemwise Recommended Reorder Level\",\n \"name\": \"Itemwise Recommended Reorder Level\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Variant Details\",\n \"name\": \"Item Variant Details\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Subcontracted Raw Materials To Be Transferred\",\n \"name\": \"Subcontracted Raw Materials To Be Transferred\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Subcontracted Item To Be Received\",\n \"name\": \"Subcontracted Item To Be Received\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Ledger Entry\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock and Account Value Comparison\",\n \"name\": \"Stock and Account Value Comparison\",\n \"type\": \"report\"\n }\n]" } ], + "cards_label": "Masters & Reports", "category": "Modules", - "charts": [], + "charts": [ + { + "chart_name": "Warehouse wise Stock Value" + } + ], "creation": "2020-03-02 15:43:10.096528", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-04-01 11:28:51.148421", + "modified": "2020-05-30 17:32:11.062681", "modified_by": "Administrator", "module": "Stock", "name": "Stock", + "onboarding": "Stock", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "shortcuts": [ { + "color": "#cef6d1", + "format": "{} Available", "label": "Item", "link_to": "Item", + "stats_filter": "{\n \"disabled\" : 0\n}", "type": "DocType" }, { - "label": "Pricing Rule", - "link_to": "Pricing Rule", + "color": "#ffe8cd", + "format": "{} Pending", + "label": "Material Request", + "link_to": "Material Request", + "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"Pending\"\n}", "type": "DocType" }, { @@ -76,6 +88,22 @@ "link_to": "Stock Entry", "type": "DocType" }, + { + "color": "#ffe8cd", + "format": "{} To Bill", + "label": "Purchase Receipt", + "link_to": "Purchase Receipt", + "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"To Bill\"\n}", + "type": "DocType" + }, + { + "color": "#ffe8cd", + "format": "{} To Bill", + "label": "Delivery Note", + "link_to": "Delivery Note", + "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"To Bill\"\n}", + "type": "DocType" + }, { "label": "Stock Ledger", "link_to": "Stock Ledger", @@ -85,6 +113,12 @@ "label": "Stock Balance", "link_to": "Stock Balance", "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "Stock", + "type": "Dashboard" } - ] + ], + "shortcuts_label": "Quick Access" } \ No newline at end of file diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index a091ac7fae..c8424f13e1 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -143,7 +143,7 @@ class Batch(Document): @frappe.whitelist() -def get_batch_qty(batch_no=None, warehouse=None, item_code=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 @@ -155,9 +155,14 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None): out = 0 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) + out = float(frappe.db.sql("""select sum(actual_qty) from `tabStock Ledger Entry` - where warehouse=%s and batch_no=%s""", + where warehouse=%s and batch_no=%s {0}""".format(cond), (warehouse, batch_no))[0][0] or 0) if batch_no and not warehouse: diff --git a/erpnext/stock/doctype/customs_tariff_number/customs_tariff_number.json b/erpnext/stock/doctype/customs_tariff_number/customs_tariff_number.json index 07992b8231..7eeb14760f 100644 --- a/erpnext/stock/doctype/customs_tariff_number/customs_tariff_number.json +++ b/erpnext/stock/doctype/customs_tariff_number/customs_tariff_number.json @@ -71,7 +71,7 @@ "read_only": 0, "remember_last_selected_value": 0, "report_hide": 0, - "reqd": 1, + "reqd": 0, "search_index": 0, "set_only_once": 0, "translatable": 0, @@ -88,7 +88,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-09-12 09:30:03.951743", + "modified": "2020-06-26 09:30:03.951743", "modified_by": "Administrator", "module": "Stock", "name": "Customs Tariff Number", @@ -143,4 +143,4 @@ "track_changes": 1, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 9f5dee901c..84d2057f96 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -24,10 +24,10 @@ "return_against", "customer_po_details", "po_no", - "section_break_18", - "pick_list", "column_break_17", "po_date", + "section_break_18", + "pick_list", "contact_info", "shipping_address_name", "shipping_address", @@ -296,7 +296,6 @@ }, { "collapsible": 1, - "collapsible_depends_on": "po_no", "fieldname": "customer_po_details", "fieldtype": "Section Break", "label": "Customer PO Details" @@ -304,7 +303,7 @@ { "allow_on_submit": 1, "fieldname": "po_no", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Customer's Purchase Order No", "no_copy": 1, "oldfieldname": "po_no", @@ -318,7 +317,6 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.po_no", "fieldname": "po_date", "fieldtype": "Date", "label": "Customer's Purchase Order Date", @@ -326,7 +324,6 @@ "oldfieldtype": "Data", "print_hide": 1, "print_width": "100px", - "read_only": 1, "width": "100px" }, { @@ -1256,7 +1253,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-04-17 12:51:41.288600", + "modified": "2020-05-19 17:03:45.880106", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index a921a56f53..4b04a0a8c3 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -537,11 +537,8 @@ class TestDeliveryNote(unittest.TestCase): dt = make_delivery_trip(dn.name) self.assertEqual(dn.name, dt.delivery_stops[0].delivery_note) - def test_delivery_note_for_enable_allow_cost_center_in_entry_of_bs_account(self): + def test_delivery_note_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 1 - accounts_settings.save() 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") @@ -567,13 +564,8 @@ class TestDeliveryNote(unittest.TestCase): } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - def test_delivery_note_for_disable_allow_cost_center_in_entry_of_bs_account(self): - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() + def test_delivery_note_cost_center_with_balance_sheet_account(self): cost_center = "Main - TCP1" company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') @@ -583,7 +575,11 @@ class TestDeliveryNote(unittest.TestCase): 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") + 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.submit() gl_entries = get_gl_entries("Delivery Note", dn.name) @@ -593,7 +589,7 @@ class TestDeliveryNote(unittest.TestCase): "cost_center": cost_center }, stock_in_hand_account: { - "cost_center": None + "cost_center": cost_center } } for i, gle in enumerate(gl_entries): 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 7ea2de2753..3d57f47601 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "hash", "creation": "2013-04-22 13:15:44", "doctype": "DocType", @@ -82,6 +81,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "section_break_72", "page_break" ], @@ -701,6 +701,12 @@ "fieldname": "dimension_col_break", "fieldtype": "Column Break" }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { "fieldname": "dn_detail", "fieldtype": "Data", @@ -714,7 +720,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-05 14:18:33.131672", + "modified": "2020-07-20 12:25:06.177894", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index c371999a27..963c87a0af 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -987,6 +987,7 @@ "read_only": 1 }, { + "collapsible": 1, "depends_on": "eval:(!doc.is_item_from_hub)", "fieldname": "hub_publishing_sb", "fieldtype": "Section Break", @@ -1060,7 +1061,7 @@ "image_field": "image", "links": [], "max_attachments": 1, - "modified": "2020-04-08 15:56:06.195722", + "modified": "2020-06-30 12:01:07.534447", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1122,4 +1123,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 7a1c1279ea..a75ee67ec4 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -597,7 +597,7 @@ class Item(WebsiteGenerator): 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 limit 1""", self.name)) + where item_code = %s and is_cancelled = 0 limit 1""", self.name)) return self._stock_ledger_created def validate_name_with_item_group(self): @@ -883,7 +883,12 @@ class Item(WebsiteGenerator): linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item"] for doctype in linked_doctypes: - if frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}) or \ + 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 + + elif frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}) or \ frappe.db.get_value("Production Order", filters={"production_item": self.name, "docstatus": 1}): return True @@ -1143,6 +1148,17 @@ def set_item_default(item_code, company, fieldname, value): d.db_insert() item.clear_cache() +@frappe.whitelist() +def get_item_details(item_code, company=None): + out = frappe._dict() + if company: + out = get_item_defaults(item_code, company) or frappe._dict() + + doc = frappe.get_cached_doc("Item", item_code) + out.update(doc.as_dict()) + + return out + @frappe.whitelist() def get_uom_conv_factor(uom, stock_uom): uoms = [uom, stock_uom] diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index 6c1a55945c..9ca887c77e 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -92,8 +92,7 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 10", - "tax_category": "" + "item_tax_template": "_Test Account Excise Duty @ 10" } ], "stock_uom": "_Test UOM 1" @@ -371,8 +370,7 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 10", - "tax_category": "" + "item_tax_template": "_Test Account Excise Duty @ 10" }, { "doctype": "Item Tax", @@ -451,14 +449,13 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 20", - "tax_category": "" + "item_tax_template": "_Test Account Excise Duty @ 20" }, { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Item Tax Template 1", - "tax_category": "_Test Tax Category 1" + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Item Tax Template 1" } ] } diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index da0c3b7f1e..522dfc67a9 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -42,6 +42,7 @@ class ItemAlternative(Document): '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() def get_alternative_items(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" (select alternative_item_code from `tabItem Alternative` where item_code = %(item_code)s and alternative_item_code like %(txt)s) @@ -52,4 +53,4 @@ def get_alternative_items(doctype, txt, searchfield, start, page_len, filters): """.format(start, page_len), { "item_code": filters.get('item_code'), "txt": '%' + txt + '%' - }) \ No newline at end of file + }) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 71b998fb95..2f75bbd97c 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -34,7 +34,7 @@ class ItemAttribute(Document): 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) + 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_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 2e0ddfdaef..5f62381f8b 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -172,7 +172,7 @@ "default": "Today", "fieldname": "valid_from", "fieldtype": "Date", - "label": "Valid From " + "label": "Valid From" }, { "default": "0", @@ -187,7 +187,7 @@ { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto " + "label": "Valid Upto" }, { "fieldname": "section_break_24", @@ -208,7 +208,7 @@ "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2020-02-28 14:21:25.580331", + "modified": "2020-07-06 22:31:32.943475", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", diff --git a/erpnext/stock/doctype/item_tax/item_tax.json b/erpnext/stock/doctype/item_tax/item_tax.json index a93e4636ad..ae36efc7e3 100644 --- a/erpnext/stock/doctype/item_tax/item_tax.json +++ b/erpnext/stock/doctype/item_tax/item_tax.json @@ -1,5 +1,4 @@ { - "actions": [], "creation": "2013-02-22 01:28:01", "doctype": "DocType", "editable_grid": 1, @@ -38,8 +37,7 @@ ], "idx": 1, "istable": 1, - "links": [], - "modified": "2019-12-28 21:54:40.807849", + "modified": "2020-06-25 01:40:28.859752", "modified_by": "Administrator", "module": "Stock", "name": "Item Tax", diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index db8bffda9d..60f5ff3629 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -18,7 +18,7 @@ frappe.ui.form.on('Material Request', { // formatter for material request item frm.set_indicator_formatter('item_code', - function(doc) { return (doc.qty<=doc.ordered_qty) ? "green" : "orange"; }); + function(doc) { return (doc.stock_qty<=doc.ordered_qty) ? "green" : "orange"; }); frm.set_query("item_code", "items", function() { return { @@ -30,7 +30,16 @@ frappe.ui.form.on('Material Request', { return { filters: {'company': doc.company} }; - }) + }); + + frm.set_query("bom_no", "items", function(doc, cdt, cdn) { + var row = locals[cdt][cdn]; + return { + filters: { + "item": row.item_code + } + } + }); }, onload: function(frm) { @@ -171,9 +180,8 @@ frappe.ui.form.on('Material Request', { }); }, - get_item_data: function(frm, item) { + get_item_data: function(frm, item, overwrite_warehouse=false) { if (item && !item.item_code) { return; } - frm.call({ method: "erpnext.stock.get_item_details.get_item_details", child: item, @@ -194,7 +202,8 @@ frappe.ui.form.on('Material Request', { plc_conversion_rate: 1, rate: item.rate, conversion_factor: item.conversion_factor - } + }, + overwrite_warehouse: overwrite_warehouse }, callback: function(r) { const d = item; @@ -345,29 +354,29 @@ frappe.ui.form.on("Material Request Item", { } const item = locals[doctype][name]; - frm.events.get_item_data(frm, item); + frm.events.get_item_data(frm, item, false); }, from_warehouse: function(frm, doctype, name) { const item = locals[doctype][name]; - frm.events.get_item_data(frm, item); + frm.events.get_item_data(frm, item, false); }, warehouse: function(frm, doctype, name) { const item = locals[doctype][name]; - frm.events.get_item_data(frm, item); + frm.events.get_item_data(frm, item, false); }, rate: function(frm, doctype, name) { const item = locals[doctype][name]; - frm.events.get_item_data(frm, item); + frm.events.get_item_data(frm, item, false); }, item_code: function(frm, doctype, name) { const item = locals[doctype][name]; item.rate = 0; set_schedule_date(frm); - frm.events.get_item_data(frm, item); + frm.events.get_item_data(frm, item, true); }, schedule_date: function(frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 97606f4e3a..25f1ed9505 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -402,6 +402,7 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa return material_requests +@frappe.whitelist() def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filters): doc = frappe.get_doc("Material Request", filters.get("doc")) item_list = [] @@ -567,4 +568,4 @@ def create_pick_list(source_name, target_doc=None): doc.set_item_locations() - return doc \ No newline at end of file + return doc 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 df140ffd75..32bd4a0a57 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -53,6 +53,8 @@ "dimension_col_break", "cost_center", "section_break_37", + "bom_no", + "section_break_46", "page_break" ], "fields": [ @@ -371,8 +373,10 @@ "label": "Image" }, { + "depends_on": "eval:parent.material_request_type == \"Manufacture\"", "fieldname": "section_break_37", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Manufacturing" }, { "fieldname": "received_qty", @@ -428,12 +432,24 @@ "fieldtype": "Link", "label": "Source Warehouse (Material Transfer)", "options": "Warehouse" + }, + { + "fieldname": "bom_no", + "fieldtype": "Link", + "label": "BOM No", + "no_copy": 1, + "options": "BOM", + "print_hide": 1 + }, + { + "fieldname": "section_break_46", + "fieldtype": "Section Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-05-01 09:00:00.992835", + "modified": "2020-05-15 09:00:00.992835", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index 7a5ae317c2..4f831d7a85 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -175,6 +175,7 @@ class PackingSlip(Document): self.update_item_details() +@frappe.whitelist() 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` diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index d46b98b461..3a5ef76980 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -31,10 +31,16 @@ frappe.ui.form.on('Pick List', { }; }); frm.set_query('item_code', 'locations', () => { + return erpnext.queries.item({ "is_stock_item": 1 }); + }); + frm.set_query('batch_no', 'locations', (frm, cdt, cdn) => { + const row = locals[cdt][cdn]; return { + query: 'erpnext.controllers.queries.get_batch_no', filters: { - is_stock_item: 1 - } + item_code: row.item_code, + warehouse: row.warehouse + }, }; }); }, diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 231af1a022..4b8b594ed9 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -24,6 +24,9 @@ class PickList(Document): for item in self.locations: 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)))) 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') @@ -116,11 +119,13 @@ def get_items_with_location_and_quantity(item_doc, item_location_map): if item_location.serial_no: serial_nos = '\n'.join(item_location.serial_no[0: cint(stock_qty)]) + auto_set_serial_no = frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo") + locations.append(frappe._dict({ 'qty': qty, 'stock_qty': stock_qty, 'warehouse': item_location.warehouse, - 'serial_no': serial_nos, + 'serial_no': serial_nos if auto_set_serial_no else item_doc.serial_no, 'batch_no': item_location.batch_no })) @@ -203,6 +208,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re sle.batch_no = batch.name and sle.`item_code`=%(item_code)s and sle.`company` = %(company)s + and batch.disabled = 0 and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s {warehouse_condition} GROUP BY @@ -300,6 +306,7 @@ def create_delivery_note(source_name, target_doc=None): 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 return delivery_note @@ -467,4 +474,4 @@ def update_common_item_properties(item, location): item.material_request = location.material_request item.serial_no = location.serial_no item.batch_no = location.batch_no - item.material_request_item = location.material_request_item \ No newline at end of file + item.material_request_item = location.material_request_item diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index e9568eeacc..50c18f6282 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -25,7 +25,7 @@ frappe.ui.form.on("Purchase Receipt", { frm.custom_make_buttons = { 'Stock Entry': 'Return', - 'Purchase Invoice': 'Invoice' + 'Purchase Invoice': 'Purchase Invoice' }; frm.set_query("expense_account", "items", function() { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 467a206d18..df9eb50843 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -1,5 +1,4 @@ { - "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-21 16:16:39", @@ -32,6 +31,8 @@ "col_break_address", "shipping_address", "shipping_address_display", + "billing_address", + "billing_address_display", "currency_and_price_list", "currency", "conversion_rate", @@ -102,6 +103,7 @@ "bill_no", "bill_date", "more_info", + "project", "status", "amended_from", "range", @@ -929,6 +931,13 @@ "print_width": "50%", "width": "50%" }, + { + "description": "Track this Purchase Receipt against any Project", + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { "fieldname": "per_billed", "fieldtype": "Percent", @@ -1079,13 +1088,25 @@ "fieldname": "scan_barcode", "fieldtype": "Data", "label": "Scan Barcode" + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Select Billing Address", + "options": "Address" + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-04-18 18:02:18.020763", + "modified": "2020-07-15 10:01:39.302238", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", @@ -1152,4 +1173,4 @@ "timeline_field": "supplier", "title_field": "title", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e6ab8d634d..d0ba001d7e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -211,6 +211,7 @@ class PurchaseReceipt(BuyingController): stock_rbnb = self.get_company_default("stock_received_but_not_billed") landed_cost_entries = get_item_account_wise_additional_cost(self.name) expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") + auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) gl_entries = [] warehouse_with_no_account = [] @@ -301,6 +302,32 @@ class PurchaseReceipt(BuyingController): 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 flt(d.qty) and auto_accounting_for_non_stock_items: + + service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") + credit_currency = get_account_currency(service_received_but_not_billed_account) + + gl_entries.append(self.get_gl_dict({ + "account": service_received_but_not_billed_account, + "against": d.expense_account, + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Service"), + "project": d.project, + "credit": d.amount, + "voucher_detail_no": d.name + }, credit_currency, item=d)) + + debit_currency = get_account_currency(d.expense_account) + + gl_entries.append(self.get_gl_dict({ + "account": d.expense_account, + "against": service_received_but_not_billed_account, + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Service"), + "project": d.project, + "debit": d.amount, + "voucher_detail_no": d.name + }, debit_currency, item=d)) self.get_asset_gl_entry(gl_entries) # Cost center-wise amount breakup for other charges included for valuation diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 649cfdcaac..d97b9e82c3 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -403,11 +403,8 @@ class TestPurchaseReceipt(unittest.TestCase): pr_return.submit() - def test_purchase_receipt_for_enable_allow_cost_center_in_entry_of_bs_account(self): + def test_purchase_receipt_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 1 - accounts_settings.save() 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") @@ -435,14 +432,7 @@ class TestPurchaseReceipt(unittest.TestCase): for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - - def test_purchase_receipt_for_disable_allow_cost_center_in_entry_of_bs_account(self): - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() - + def test_purchase_receipt_cost_center_with_balance_sheet_account(self): if not frappe.db.exists('Location', 'Test Location'): frappe.get_doc({ 'doctype': 'Location', @@ -454,13 +444,14 @@ class TestPurchaseReceipt(unittest.TestCase): gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) + cost_center = pr.get('items')[0].cost_center expected_values = { "Stock Received But Not Billed - TCP1": { - "cost_center": None + "cost_center": cost_center }, stock_in_hand_account: { - "cost_center": None + "cost_center": cost_center } } for i, gle in enumerate(gl_entries): diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index a9f3cd09ef..c951066aa8 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2013-04-30 13:13:03", "doctype": "DocType", @@ -8,6 +9,7 @@ "field_order": [ "naming_series", "report_date", + "status", "column_break_4", "inspection_type", "reference_type", @@ -20,17 +22,16 @@ "column_break1", "item_name", "description", - "status", + "bom_no", + "specification_details", + "quality_inspection_template", + "readings", "section_break_14", "inspected_by", "verified_by", - "bom_no", "column_break_17", "remarks", - "amended_from", - "specification_details", - "quality_inspection_template", - "readings" + "amended_from" ], "fields": [ { @@ -231,7 +232,8 @@ "icon": "fa fa-search", "idx": 1, "is_submittable": 1, - "modified": "2019-07-12 12:07:23.153698", + "links": [], + "modified": "2020-04-26 17:50:25.068222", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 37ab807cb7..568e742876 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -58,6 +58,7 @@ class QualityInspection(Document): .format(parent_doc=self.reference_type, child_doc=doctype), (quality_inspection, self.modified, self.reference_name, self.item_code)) +@frappe.whitelist() def item_query(doctype, txt, searchfield, start, page_len, filters): if filters.get("from"): from frappe.desk.reportview import get_match_cond @@ -86,6 +87,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): page_len = page_len, qi_condition = qi_condition), {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt}) +@frappe.whitelist() def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters): return frappe.get_all('Quality Inspection', limit_start=start, @@ -118,4 +120,4 @@ def make_quality_inspection(source_name, target_doc=None): } }, target_doc, postprocess) - return doc \ No newline at end of file + return doc diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 731a730279..2be14c8006 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -1,7 +1,6 @@ { "actions": [], "allow_import": 1, - "allow_rename": 1, "autoname": "field:serial_no", "creation": "2013-05-16 10:59:15", "description": "Distinct unit of an Item", @@ -420,14 +419,14 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", - "options": "\nActive\nDelivered\nExpired", + "options": "\nActive\nInactive\nDelivered\nExpired", "read_only": 1 } ], "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2020-04-08 13:29:58.517772", + "modified": "2020-06-25 15:53:50.900855", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 914eea379a..90f0f5881d 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -42,6 +42,8 @@ class SerialNo(StockController): self.status = "Delivered" elif self.warranty_expiry_date and getdate(self.warranty_expiry_date) <= getdate(nowdate()): self.status = "Expired" + elif not self.warehouse: + self.status = "Inactive" else: self.status = "Active" @@ -188,23 +190,6 @@ class SerialNo(StockController): if sle_exists: 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: - frappe.throw(_("Sorry, Serial Nos cannot be merged")) - - 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')"""): - - 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])) - def update_serial_no_reference(self, serial_no=None): last_sle = self.get_last_sle(serial_no) self.set_purchase_details(last_sle.get("purchase_sle")) diff --git a/erpnext/stock/doctype/serial_no/serial_no_list.js b/erpnext/stock/doctype/serial_no/serial_no_list.js index 651f790583..7526d1d8a5 100644 --- a/erpnext/stock/doctype/serial_no/serial_no_list.js +++ b/erpnext/stock/doctype/serial_no/serial_no_list.js @@ -5,6 +5,8 @@ frappe.listview_settings['Serial No'] = { return [__("Delivered"), "green", "delivery_document_type,is,set"]; } else if (doc.warranty_expiry_date && frappe.datetime.get_diff(doc.warranty_expiry_date, frappe.datetime.nowdate()) <= 0) { return [__("Expired"), "red", "warranty_expiry_date,not in,|warranty_expiry_date,<=,Today|delivery_document_type,is,not set"]; + } else if (!doc.warehouse) { + return [__("Inactive"), "grey", "warehouse,is,not set"]; } else { return [__("Active"), "green", "delivery_document_type,is,not set"]; } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index be2dd526a6..229cf027bd 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -500,7 +500,7 @@ class StockEntry(StockController): if raw_material_cost and self.purpose == "Manufacture": d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate")) d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) - elif self.purpose == "Repack" and total_fg_qty: + elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually: d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) d.basic_amount = d.basic_rate * d.qty @@ -574,9 +574,7 @@ class StockEntry(StockController): {"parent": self.purchase_order, "item_code": se_item.subcontracted_item}, "bom") - allow_alternative_item = frappe.get_value("BOM", bom_no, "allow_alternative_item") - - if allow_alternative_item: + if se_item.allow_alternative_item: 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 \ @@ -732,14 +730,18 @@ class StockEntry(StockController): pro_doc = frappe.get_doc("Work Order", self.work_order) _validate_work_order(pro_doc) pro_doc.run_method("update_status") + if self.fg_completed_qty: pro_doc.run_method("update_work_order_qty") if self.purpose == "Manufacture": pro_doc.run_method("update_planned_qty") + if not pro_doc.operations: + pro_doc.set_actual_dates() + 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, - i.has_batch_no, i.sample_quantity, i.has_serial_no, + 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 @@ -774,6 +776,9 @@ class StockEntry(StockController): 'expense_account' : item.expense_account }) + 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'))) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index c16a41c24f..7b9c129804 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -23,6 +23,7 @@ "image", "image_view", "quantity_and_rate", + "set_basic_rate_manually", "qty", "basic_rate", "basic_amount", @@ -491,12 +492,21 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse", + "fieldname": "set_basic_rate_manually", + "fieldtype": "Check", + "label": "Set Basic Rate Manually", + "show_days": 1, + "show_seconds": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-23 19:19:28.539769", + "modified": "2020-06-08 12:57:03.172887", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index dd284e4a96..ecee97ce86 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -74,6 +74,20 @@ frappe.ui.form.on("Stock Reconciliation", { , __("Get Items"), __("Update")); }, + posting_date: function(frm) { + frm.trigger("set_valuation_rate_and_qty_for_all_items"); + }, + + posting_time: function(frm) { + frm.trigger("set_valuation_rate_and_qty_for_all_items"); + }, + + set_valuation_rate_and_qty_for_all_items: function(frm) { + frm.doc.items.forEach(row => { + frm.events.set_valuation_rate_and_qty(frm, row.doctype, row.name); + }); + }, + set_valuation_rate_and_qty: function(frm, cdt, cdn) { var d = frappe.model.get_doc(cdt, cdn); diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 5e469c24d7..43fbc00466 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -184,8 +184,12 @@ class StockReconciliation(StockController): sl_entries = [] has_serial_no = False + has_batch_no = False for row in self.items: item = frappe.get_doc("Item", row.item_code) + if item.has_batch_no: + has_batch_no = True + if item.has_serial_no or item.has_batch_no: has_serial_no = True self.get_sle_for_serialized_items(row, sl_entries) @@ -222,7 +226,11 @@ class StockReconciliation(StockController): if has_serial_no: sl_entries = self.merge_similar_item_serial_nos(sl_entries) - self.make_sl_entries(sl_entries) + allow_negative_stock = False + if has_batch_no: + allow_negative_stock = True + + self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) if has_serial_no and sl_entries: self.update_valuation_rate_for_serial_no() @@ -493,7 +501,7 @@ 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) or 0 + qty = get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 return { 'qty': qty, diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index cc0e2cfc42..48624e0f25 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -15,3 +15,37 @@ frappe.ui.form.on('Stock Settings', { frm.set_query("sample_retention_warehouse", filters); } }); + +frappe.tour['Stock Settings'] = [ + { + fieldname: "item_naming_by", + title: __("Item Naming By"), + description: __("By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a ") + "Naming Series" + __(" choose the 'Naming Series' option."), + }, + { + fieldname: "default_warehouse", + title: __("Default Warehouse"), + description: __("Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.") + }, + { + fieldname: "allow_negative_stock", + title: __("Allow Negative Stock"), + description: __("This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.") + + }, + { + fieldname: "valuation_method", + title: __("Valuation Method"), + description: __("Choose between FIFO and Moving Average Valuation Methods. Click ") + "here" + __(" to know more about them.") + }, + { + fieldname: "show_barcode_field", + title: __("Show Barcode Field"), + description: __("Show 'Scan Barcode' field above every child table to insert Items with ease.") + }, + { + fieldname: "automatically_set_serial_nos_based_on_fifo", + title: __("Automatically Set Serial Nos based on FIFO"), + description: __("Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.") + } +]; diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index c9a3527e91..9c5d3d8340 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -216,7 +216,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2020-04-01 18:11:25.417678", + "modified": "2020-06-20 11:39:15.344112", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 11b6403419..b8554c83e2 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -47,6 +47,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru """ args = process_args(args) + for_validate = process_string_args(for_validate) + overwrite_warehouse = process_string_args(overwrite_warehouse) item = frappe.get_cached_doc("Item", args.item_code) validate_item_details(args, item) @@ -166,6 +168,10 @@ 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): @@ -305,7 +311,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): "weight_uom":item.weight_uom, "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") + "against_blanket_order": args.get("against_blanket_order"), + "bom_no": item.get("default_bom") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): @@ -412,7 +419,7 @@ def get_item_tax_info(company, tax_category, item_codes): continue out[item_code] = {} item = frappe.get_cached_doc("Item", item_code) - get_item_tax_template({"tax_category": tax_category}, item, out[item_code]) + get_item_tax_template({"company": company, "tax_category": tax_category}, item, out[item_code]) out[item_code]["item_tax_rate"] = get_item_tax_map(company, out[item_code].get("item_tax_template"), as_json=True) return out @@ -441,7 +448,8 @@ def _get_item_tax_template(args, taxes, out={}, for_validate=False): taxes_with_no_validity = [] for tax in taxes: - if tax.valid_from: + tax_company = frappe.get_value("Item Tax Template", tax.item_tax_template, 'company') + if tax.valid_from and tax_company == args['company']: # 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') @@ -449,7 +457,8 @@ def _get_item_tax_template(args, taxes, out={}, for_validate=False): if getdate(tax.valid_from) <= getdate(validation_date): taxes_with_validity.append(tax) else: - taxes_with_no_validity.append(tax) + if tax_company == args['company']: + taxes_with_no_validity.append(tax) if taxes_with_validity: taxes = sorted(taxes_with_validity, key = lambda i: i.valid_from, reverse=True) @@ -636,6 +645,10 @@ def get_item_price(args, item_code, ignore_party=False): conditions += """ and %(transaction_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" + 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 from `tabItem Price` {conditions} order by valid_from desc, uom desc """.format(conditions=conditions), args) @@ -656,6 +669,7 @@ def get_price_list_rate_for(args, item_code): "supplier": args.get('supplier'), "uom": args.get('uom'), "transaction_date": args.get('transaction_date'), + "posting_date": args.get('posting_date'), } item_price_data = 0 diff --git a/erpnext/stock/module_onboarding/stock/stock.json b/erpnext/stock/module_onboarding/stock/stock.json new file mode 100644 index 0000000000..1d5bf8c97c --- /dev/null +++ b/erpnext/stock/module_onboarding/stock/stock.json @@ -0,0 +1,53 @@ +{ + "allow_roles": [ + { + "role": "Manufacturing Manager" + }, + { + "role": "Stock Manager" + }, + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + } + ], + "creation": "2020-05-15 03:18:44.400108", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-08 14:22:07.951891", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock", + "owner": "Administrator", + "steps": [ + { + "step": "Setup your Warehouse" + }, + { + "step": "Create a Product" + }, + { + "step": "Create a Supplier" + }, + { + "step": "Introduction to Stock Entry" + }, + { + "step": "Create a Stock Entry" + }, + { + "step": "Create a Purchase Receipt" + }, + { + "step": "Stock Settings" + } + ], + "subtitle": "Inventory, Warehouses, Analysis, and more.", + "success_message": "The Stock Module is all set up!", + "title": "Let's Set Up the Stock Module." +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/buying_settings/buying_settings.json b/erpnext/stock/onboarding_step/buying_settings/buying_settings.json new file mode 100644 index 0000000000..a788ccd4cc --- /dev/null +++ b/erpnext/stock/onboarding_step/buying_settings/buying_settings.json @@ -0,0 +1,19 @@ +{ + "action": "Update Settings", + "creation": "2020-05-06 15:53:44.667414", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-12 18:30:06.323797", + "modified_by": "Administrator", + "name": "Buying Settings", + "owner": "Administrator", + "reference_document": "Buying Settings", + "show_full_form": 0, + "title": "Configure Buying Settings.", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_product/create_a_product.json b/erpnext/stock/onboarding_step/create_a_product/create_a_product.json new file mode 100644 index 0000000000..d2068e167b --- /dev/null +++ b/erpnext/stock/onboarding_step/create_a_product/create_a_product.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-12 18:16:06.624554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-12 18:30:02.489949", + "modified_by": "Administrator", + "name": "Create a Product", + "owner": "Administrator", + "reference_document": "Item", + "show_full_form": 0, + "title": "Create a Product", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json b/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json new file mode 100644 index 0000000000..b7811a46df --- /dev/null +++ b/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-19 18:59:13.266713", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-19 18:59:13.266713", + "modified_by": "Administrator", + "name": "Create a Purchase Receipt", + "owner": "Administrator", + "reference_document": "Purchase Receipt", + "show_full_form": 1, + "title": "Create a Purchase Receipt", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json new file mode 100644 index 0000000000..2b83f657d6 --- /dev/null +++ b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-15 03:20:16.277043", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-15 03:30:58.047696", + "modified_by": "Administrator", + "name": "Create a Stock Entry", + "owner": "Administrator", + "reference_document": "Stock Entry", + "show_full_form": 1, + "title": "Create a Stock Entry", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json new file mode 100644 index 0000000000..7a64224bd4 --- /dev/null +++ b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 22:09:10.043554", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-14 22:09:10.043554", + "modified_by": "Administrator", + "name": "Create a Supplier", + "owner": "Administrator", + "reference_document": "Supplier", + "show_full_form": 0, + "title": "Create a Supplier", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json new file mode 100644 index 0000000000..009a44f6e4 --- /dev/null +++ b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json @@ -0,0 +1,19 @@ +{ + "action": "Watch Video", + "creation": "2020-05-15 02:47:17.958806", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-05-26 15:55:41.457289", + "modified_by": "Administrator", + "name": "Introduction to Stock Entry", + "owner": "Administrator", + "show_full_form": 0, + "title": "Introduction to Stock Entry", + "validate_action": 1, + "video_url": "https://www.youtube.com/watch?v=Njt107hlY3I" +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json new file mode 100644 index 0000000000..9457deee26 --- /dev/null +++ b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -0,0 +1,20 @@ +{ + "action": "Go to Page", + "creation": "2020-05-19 18:54:19.383397", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-04 12:33:16.970031", + "modified_by": "Administrator", + "name": "Setup your Warehouse", + "owner": "Administrator", + "path": "Tree/Warehouse", + "reference_document": "Warehouse", + "show_full_form": 0, + "title": "Set up your Warehouse", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json new file mode 100644 index 0000000000..7591bff538 --- /dev/null +++ b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json @@ -0,0 +1,19 @@ +{ + "action": "Show Form Tour", + "creation": "2020-05-15 02:53:57.209967", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 1, + "is_skipped": 0, + "modified": "2020-05-15 03:55:15.444151", + "modified_by": "Administrator", + "name": "Stock Settings", + "owner": "Administrator", + "reference_document": "Stock Settings", + "show_full_form": 0, + "title": "Explore Stock Settings", + "validate_action": 1 +} \ No newline at end of file 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 27cf6b66cc..446d3049b7 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +from frappe import _ from erpnext.controllers.trends import get_columns,get_data def execute(filters=None): @@ -10,5 +11,40 @@ def execute(filters=None): data = [] conditions = get_columns(filters, "Delivery Note") data = get_data(filters, conditions) - - return conditions["columns"], data \ No newline at end of file + + chart_data = get_chart_data(data, filters) + + return conditions["columns"], data, None, chart_data + +def get_chart_data(data, filters): + if not data: + return [] + + labels, datapoints = [], [] + + if filters.get("group_by"): + # consider only consolidated row + data = [row for row in data if row[0]] + + data = sorted(data, key = lambda i: i[-1],reverse=True) + + if len(data) > 10: + # get top 10 if data too long + data = data[:10] + + for row in data: + labels.append(row[0]) + datapoints.append(row[-1]) + + return { + "data": { + "labels" : labels, + "datasets" : [ + { + "name": _("Total Delivered Amount"), + "values": datapoints + } + ] + }, + "type" : "bar" + } \ No newline at end of file diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.js b/erpnext/stock/report/item_shortage_report/item_shortage_report.js new file mode 100644 index 0000000000..ca42a331e9 --- /dev/null +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.js @@ -0,0 +1,26 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Item Shortage Report"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "width": "80", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_default("company") + }, + { + "fieldname": "warehouse", + "label": __("Warehouse"), + "fieldtype": "MultiSelectList", + "width": "100", + get_data: function(txt) { + return frappe.db.get_link_options('Warehouse', txt); + } + } + ] +}; diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.json b/erpnext/stock/report/item_shortage_report/item_shortage_report.json index 577a8530b7..17285c09de 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.json +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.json @@ -1,29 +1,30 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-08-20 13:43:30", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "json": "{\"add_total_row\": 0, \"sort_by\": \"Bin.projected_qty\", \"sort_order\": \"asc\", \"sort_by_next\": \"\", \"filters\": [[\"Bin\", \"projected_qty\", \"<\", \"0\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"warehouse\", \"Bin\"], [\"item_code\", \"Bin\"], [\"actual_qty\", \"Bin\"], [\"ordered_qty\", \"Bin\"], [\"planned_qty\", \"Bin\"], [\"reserved_qty\", \"Bin\"], [\"projected_qty\", \"Bin\"]]}", - "modified": "2017-02-24 20:00:46.439935", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Shortage Report", - "owner": "Administrator", - "query": "SELECT bin.warehouse as \"Warehouse:Link/Warehouse:150\",\n\tbin.item_code as \"Item Code:Link/Item:100\",\n\tbin.actual_qty as \"Actual Quantity:Float:120\",\n\tbin.ordered_qty as \"Ordered Quantity:Float:120\",\n\tbin.planned_qty as \"Planned Quantity:Float:120\",\n\tbin.reserved_qty as \"Reserved Quantity:Float:120\",\n\tbin.projected_qty as \"Project Quantity:Float:120\",\n\titem.item_name as \"Item Name:Data:150\",\n\titem.description as \"Description::200\"\nFROM tabBin as bin\nINNER JOIN tabItem as item\nON bin.item_code=item.name\nWHERE bin.projected_qty<0\nORDER BY bin.projected_qty;", - "ref_doctype": "Bin", - "report_name": "Item Shortage Report", - "report_type": "Query Report", + "add_total_row": 0, + "creation": "2013-08-20 13:43:30", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 3, + "is_standard": "Yes", + "json": "{\"add_total_row\": 0, \"sort_by\": \"Bin.projected_qty\", \"sort_order\": \"asc\", \"sort_by_next\": \"\", \"filters\": [[\"Bin\", \"projected_qty\", \"<\", \"0\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"warehouse\", \"Bin\"], [\"item_code\", \"Bin\"], [\"actual_qty\", \"Bin\"], [\"ordered_qty\", \"Bin\"], [\"planned_qty\", \"Bin\"], [\"reserved_qty\", \"Bin\"], [\"projected_qty\", \"Bin\"]]}", + "modified": "2020-05-14 12:32:07.158991", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Shortage Report", + "owner": "Administrator", + "prepared_report": 0, + "query": "", + "ref_doctype": "Bin", + "report_name": "Item Shortage Report", + "report_type": "Script Report", "roles": [ { "role": "Sales User" - }, + }, { "role": "Purchase User" - }, + }, { "role": "Stock User" } diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py new file mode 100644 index 0000000000..086d833bbc --- /dev/null +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py @@ -0,0 +1,162 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns() + conditions = get_conditions(filters) + data = get_data(conditions, filters) + + if not data: + return [], [], None, [] + + chart_data = get_chart_data(data) + + return columns, data, None, chart_data + +def get_conditions(filters): + conditions = "" + + if filters.get("warehouse"): + conditions += "AND warehouse in %(warehouse)s" + if filters.get("company"): + conditions += "AND company = %(company)s" + + return conditions + +def get_data(conditions, filters): + data = frappe.db.sql(""" + SELECT + bin.warehouse, + bin.item_code, + bin.actual_qty , + bin.ordered_qty , + bin.planned_qty , + bin.reserved_qty , + bin.reserved_qty_for_production, + bin.projected_qty , + warehouse.company, + item.item_name , + item.description + FROM + `tabBin` bin, + `tabWarehouse` warehouse, + `tabItem` item + WHERE + bin.projected_qty<0 + AND warehouse.name = bin.warehouse + AND bin.item_code=item.name + {0} + ORDER BY bin.projected_qty;""".format(conditions), filters, as_dict=1) + + return data + +def get_chart_data(data): + labels, datapoints = [], [] + + for row in data: + labels.append(row.get("item_code")) + datapoints.append(row.get("projected_qty")) + + if len(data) > 10: + labels = labels[:10] + datapoints = datapoints[:10] + + return { + "data": { + "labels": labels, + "datasets":[ + { + "name": _("Projected Qty"), + "values": datapoints + } + ] + }, + "type": "bar" + } + +def get_columns(): + columns = [ + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 150 + }, + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 150 + }, + { + "label": _("Actual Quantity"), + "fieldname": "actual_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty" + }, + { + "label": _("Ordered Quantity"), + "fieldname": "ordered_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty" + }, + { + "label": _("Planned Quantity"), + "fieldname": "planned_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty" + }, + { + "label": _("Reserved Quantity"), + "fieldname": "reserved_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty" + }, + { + "label": _("Reserved Quantity for Production"), + "fieldname": "reserved_qty_for_production", + "fieldtype": "Float", + "width": 120, + "convertible": "qty" + }, + { + "label": _("Projected Quantity"), + "fieldname": "projected_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty" + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "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/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index 9a972104a2..5df3fa8067 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 @@ -8,7 +8,7 @@ from frappe.utils import getdate, flt def execute(filters=None): if not filters: filters = {} - float_preceision = frappe.db.get_default("float_preceision") + float_precision = frappe.db.get_default("float_precision") condition = get_condition(filters) @@ -25,7 +25,7 @@ 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)) - avg_daily_outgoing = flt(total_outgoing / diff, float_preceision) + 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, diff --git a/erpnext/stock/report/ordered_items_to_be_delivered/ordered_items_to_be_delivered.json b/erpnext/stock/report/ordered_items_to_be_delivered/ordered_items_to_be_delivered.json deleted file mode 100644 index aa5fd0f165..0000000000 --- a/erpnext/stock/report/ordered_items_to_be_delivered/ordered_items_to_be_delivered.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "add_total_row": 1, - "creation": "2018-01-09 18:38:23.540100", - "disable_prepared_report": 0, - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2019-04-01 22:10:09.829361", - "modified_by": "Administrator", - "module": "Stock", - "name": "Ordered Items To Be Delivered", - "owner": "Administrator", - "prepared_report": 0, - "query": "select \n `tabSales Order`.`name` as \"Sales Order:Link/Sales Order:120\",\n `tabSales Order`.`status` as \"Status:Data:120\",\n `tabSales Order`.`customer` as \"Customer:Link/Customer:120\",\n `tabSales Order`.`customer_name` as \"Customer Name::150\",\n `tabSales Order`.`transaction_date` as \"Date:Date\",\n `tabSales Order`.`project` as \"Project:Link/Project:120\",\n `tabSales Order Item`.item_code as \"Item:Link/Item:120\",\n `tabSales Order Item`.qty as \"Qty:Float:140\",\n `tabSales Order Item`.delivered_qty as \"Delivered Qty:Float:140\",\n (`tabSales Order Item`.qty - ifnull(`tabSales Order Item`.delivered_qty, 0)) as \"Qty to Deliver:Float:140\",\n `tabSales Order Item`.base_rate as \"Rate:Float:140\",\n `tabSales Order Item`.base_amount as \"Amount:Float:140\",\n ((`tabSales Order Item`.qty - ifnull(`tabSales Order Item`.delivered_qty, 0))*`tabSales Order Item`.base_rate) as \"Amount to Deliver:Float:140\",\n `tabBin`.actual_qty as \"Available Qty:Float:120\",\n `tabBin`.projected_qty as \"Projected Qty:Float:120\",\n `tabSales Order Item`.`delivery_date` as \"Item Delivery Date:Date:120\",\n DATEDIFF(CURDATE(),`tabSales Order Item`.`delivery_date`) as \"Delay Days:Int:120\",\n `tabSales Order Item`.item_name as \"Item Name::150\",\n `tabSales Order Item`.description as \"Description::200\",\n `tabSales Order Item`.item_group as \"Item Group:Link/Item Group:120\",\n `tabSales Order Item`.warehouse as \"Warehouse:Link/Warehouse:200\"\nfrom\n `tabSales Order` JOIN `tabSales Order Item` \n LEFT JOIN `tabBin` ON (`tabBin`.item_code = `tabSales Order Item`.item_code\n and `tabBin`.warehouse = `tabSales Order Item`.warehouse)\nwhere\n `tabSales Order Item`.`parent` = `tabSales Order`.`name`\n and `tabSales Order`.docstatus = 1\n and `tabSales Order`.status not in (\"Stopped\", \"Closed\")\n and ifnull(`tabSales Order Item`.delivered_qty,0) < ifnull(`tabSales Order Item`.qty,0)\norder by `tabSales Order`.transaction_date asc", - "ref_doctype": "Delivery Note", - "report_name": "Ordered Items To Be Delivered", - "report_type": "Query Report", - "roles": [ - { - "role": "Stock User" - }, - { - "role": "Stock Manager" - }, - { - "role": "Sales User" - }, - { - "role": "Accounts User" - } - ] -} \ No newline at end of file diff --git a/erpnext/stock/report/purchase_order_items_to_be_received/purchase_order_items_to_be_received.json b/erpnext/stock/report/purchase_order_items_to_be_received/purchase_order_items_to_be_received.json deleted file mode 100644 index dfaa9ed6cc..0000000000 --- a/erpnext/stock/report/purchase_order_items_to_be_received/purchase_order_items_to_be_received.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "add_total_row": 1, - "creation": "2013-02-22 18:01:55", - "disable_prepared_report": 0, - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2019-04-01 22:12:05.573343", - "modified_by": "Administrator", - "module": "Stock", - "name": "Purchase Order Items To Be Received", - "owner": "Administrator", - "prepared_report": 0, - "query": "select \n `tabPurchase Order`.`name` as \"Purchase Order:Link/Purchase Order:120\",\n `tabPurchase Order`.`status` as \"Status:Data:120\",\n\t`tabPurchase Order`.`transaction_date` as \"Date:Date:100\",\n\t`tabPurchase Order Item`.`schedule_date` as \"Reqd by Date:Date:110\",\n\t`tabPurchase Order`.`supplier` as \"Supplier:Link/Supplier:120\",\n\t`tabPurchase Order`.`supplier_name` as \"Supplier Name::150\",\n\t`tabPurchase Order Item`.`project` as \"Project\",\n\t`tabPurchase Order Item`.item_code as \"Item Code:Link/Item:120\",\n\t`tabPurchase Order Item`.qty as \"Qty:Float:100\",\n\t`tabPurchase Order Item`.received_qty as \"Received Qty:Float:100\", \n\t(`tabPurchase Order Item`.qty - ifnull(`tabPurchase Order Item`.received_qty, 0)) as \"Qty to Receive:Float:100\",\n `tabPurchase Order Item`.warehouse as \"Warehouse:Link/Warehouse:150\",\n\t`tabPurchase Order Item`.item_name as \"Item Name::150\",\n\t`tabPurchase Order Item`.description as \"Description::200\",\n `tabPurchase Order Item`.brand as \"Brand::100\",\n\t`tabPurchase Order`.`company` as \"Company:Link/Company:\"\nfrom\n\t`tabPurchase Order`, `tabPurchase Order Item`\nwhere\n\t`tabPurchase Order Item`.`parent` = `tabPurchase Order`.`name`\n\tand `tabPurchase Order`.docstatus = 1\n\tand `tabPurchase Order`.status not in (\"Stopped\", \"Closed\")\n\tand ifnull(`tabPurchase Order Item`.received_qty, 0) < ifnull(`tabPurchase Order Item`.qty, 0)\norder by `tabPurchase Order`.transaction_date asc", - "ref_doctype": "Purchase Receipt", - "report_name": "Purchase Order Items To Be Received", - "report_type": "Query Report", - "roles": [ - { - "role": "Stock Manager" - }, - { - "role": "Stock User" - }, - { - "role": "Purchase User" - }, - { - "role": "Accounts User" - } - ] -} \ No newline at end of file diff --git a/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json b/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json deleted file mode 100644 index 48c0f423fd..0000000000 --- a/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "add_total_row": 0, - "creation": "2019-09-16 14:10:33.102865", - "disable_prepared_report": 0, - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2019-09-21 15:19:55.710578", - "modified_by": "Administrator", - "module": "Stock", - "name": "Purchase Order Items To Be Received or Billed", - "owner": "Administrator", - "prepared_report": 0, - "query": "SELECT\n\t`poi_pri`.`purchase_order` as \"Purchase Order:Link/Purchase Order:120\",\n\t`poi_pri`.`status` as \"Status:Data:120\",\n\t`poi_pri`.`transaction_date` as \"Date:Date:100\",\n\t`poi_pri`.`schedule_date` as \"Reqd by Date:Date:110\",\n\t`poi_pri`.`supplier` as \"Supplier:Link/Supplier:120\",\n\t`poi_pri`.`supplier_name` as \"Supplier Name::150\",\n\t`poi_pri`.`item_code` as \"Item Code:Link/Item:120\",\n\t`poi_pri`.`qty` as \"Qty:Float:100\",\n\t`poi_pri`.`base_amount` as \"Base Amount:Currency:100\",\n\t`poi_pri`.`received_qty` as \"Received Qty:Float:100\",\n\t`poi_pri`.`received_amount` as \"Received Qty Amount:Currency:100\",\n\t`poi_pri`.`qty_to_receive` as \"Qty to Receive:Float:100\",\n\t`poi_pri`.`amount_to_be_received` as \"Amount to Receive:Currency:100\",\n\t`poi_pri`.`billed_amount` as \"Billed Amount:Currency:100\",\n\t`poi_pri`.`amount_to_be_billed` as \"Amount To Be Billed:Currency:100\",\n\tSUM(`pii`.`qty`) AS \"Billed Qty:Float:100\",\n\t`poi_pri`.qty - SUM(`pii`.`qty`) AS \"Qty To Be Billed:Float:100\",\n\t`poi_pri`.`warehouse` as \"Warehouse:Link/Warehouse:150\",\n\t`poi_pri`.`item_name` as \"Item Name::150\",\n\t`poi_pri`.`description` as \"Description::200\",\n\t`poi_pri`.`brand` as \"Brand::100\",\n\t`poi_pri`.`project` as \"Project\",\n\t`poi_pri`.`company` as \"Company:Link/Company:\"\nFROM\n\t(SELECT\n\t\t`po`.`name` AS 'purchase_order',\n\t\t`po`.`status`,\n\t\t`po`.`company`,\n\t\t`poi`.`warehouse`,\n\t\t`poi`.`brand`,\n\t\t`poi`.`description`,\n\t\t`po`.`transaction_date`,\n\t\t`poi`.`schedule_date`,\n\t\t`po`.`supplier`,\n\t\t`po`.`supplier_name`,\n\t\t`poi`.`project`,\n\t\t`poi`.`item_code`,\n\t\t`poi`.`item_name`,\n\t\t`poi`.`qty`,\n\t\t`poi`.`base_amount`,\n\t\t`poi`.`received_qty`,\n\t\t(`poi`.billed_amt * ifnull(`po`.conversion_rate, 1)) as billed_amount,\n\t\t(`poi`.base_amount - (`poi`.billed_amt * ifnull(`po`.conversion_rate, 1))) as amount_to_be_billed,\n\t\t`poi`.`qty` - IFNULL(`poi`.`received_qty`, 0) AS 'qty_to_receive',\n\t\t(`poi`.`qty` - IFNULL(`poi`.`received_qty`, 0)) * `poi`.`rate` AS 'amount_to_be_received',\n\t\tSUM(`pri`.`amount`) AS 'received_amount',\n\t\t`poi`.`name` AS 'poi_name',\n\t\t`pri`.`name` AS 'pri_name'\n\tFROM\n\t\t`tabPurchase Order` po\n\t\tLEFT JOIN `tabPurchase Order Item` poi\n\t\tON `poi`.`parent` = `po`.`name`\n\t\tLEFT JOIN `tabPurchase Receipt Item` pri\n\t\tON `pri`.`purchase_order_item` = `poi`.`name`\n\t\t\tAND `pri`.`docstatus`=1\n\tWHERE\n\t\t`po`.`status` not in ('Stopped', 'Closed')\n\t\tAND `po`.`docstatus` = 1\n\t\tAND IFNULL(`poi`.`received_qty`, 0) < IFNULL(`poi`.`qty`, 0)\n\tGROUP BY `poi`.`name`\n\tORDER BY `po`.`transaction_date` ASC\n\t) poi_pri\n\tLEFT JOIN `tabPurchase Invoice Item` pii\n\tON `pii`.`po_detail` = `poi_pri`.`poi_name`\n\t\tAND `pii`.`docstatus`=1\nGROUP BY `poi_pri`.`poi_name`", - "ref_doctype": "Purchase Order", - "report_name": "Purchase Order Items To Be Received or Billed", - "report_type": "Query Report", - "roles": [ - { - "role": "Purchase Manager" - }, - { - "role": "Purchase User" - }, - { - "role": "Stock User" - }, - { - "role": "Stock Manager" - } - ] -} \ No newline at end of file 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 0e58920725..8227f1548c 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +from frappe import _ from erpnext.controllers.trends import get_columns,get_data def execute(filters=None): @@ -11,4 +12,40 @@ def execute(filters=None): conditions = get_columns(filters, "Purchase Receipt") data = get_data(filters, conditions) - return conditions["columns"], data \ No newline at end of file + chart_data = get_chart_data(data, filters) + + return conditions["columns"], data, None, chart_data + +def get_chart_data(data, filters): + if not data: + return [] + + labels, datapoints = [], [] + + if filters.get("group_by"): + # consider only consolidated row + data = [row for row in data if row[0]] + + data = sorted(data, key = lambda i: i[-1], reverse=True) + + if len(data) > 10: + # get top 10 if data too long + data = data[:10] + + for row in data: + labels.append(row[0]) + datapoints.append(row[-1]) + + return { + "data": { + "labels" : labels, + "datasets" : [ + { + "name": _("Total Received Amount"), + "values": datapoints + } + ] + }, + "type" : "bar", + "colors":["#5e64ff"] + } \ No newline at end of file diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 803a5c81a3..d5878cb662 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -37,7 +37,9 @@ def execute(filters=None): data.append(row) - return columns, data + chart_data = get_chart_data(data, filters) + + return columns, data, None, chart_data def get_average_age(fifo_queue, to_date): batch_age = age_qty = total_qty = 0.0 @@ -51,7 +53,7 @@ def get_average_age(fifo_queue, to_date): age_qty += batch_age * 1 total_qty += 1 - return (age_qty / total_qty) if total_qty else 0.0 + return flt(age_qty / total_qty, 2) if total_qty else 0.0 def get_columns(filters): columns = [ @@ -178,14 +180,14 @@ def get_fifo_queue(filters, sle=None): qty_to_pop = abs(d.actual_qty) while qty_to_pop: batch = fifo_queue[0] if fifo_queue else [0, None] - if 0 < batch[0] <= qty_to_pop: + if 0 < flt(batch[0]) <= qty_to_pop: # if batch qty > 0 # not enough or exactly same qty in current batch, clear batch - qty_to_pop -= batch[0] + qty_to_pop -= flt(batch[0]) transferred_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0)) else: # all from current batch - batch[0] -= qty_to_pop + batch[0] = flt(batch[0]) - qty_to_pop transferred_item_details[(d.voucher_no, d.name)].append([qty_to_pop, batch[1]]) qty_to_pop = 0 @@ -230,3 +232,34 @@ def get_sle_conditions(filters): where wh.lft >= {0} and rgt <= {1})""".format(lft, rgt)) return "and {}".format(" and ".join(conditions)) if conditions else "" + +def get_chart_data(data, filters): + if not data: + return [] + + labels, datapoints = [], [] + + if filters.get("show_warehouse_wise_stock"): + return {} + + data.sort(key = lambda row: row[6], reverse=True) + + if len(data) > 10: + data = data[:10] + + for row in data: + labels.append(row[0]) + datapoints.append(row[6]) + + return { + "data" : { + "labels": labels, + "datasets": [ + { + "name": _("Average Age"), + "values": datapoints + } + ] + }, + "type" : "bar" + } diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 74a4f6ef14..042087a4a7 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe import _ from frappe.utils import flt, cint, getdate, now, date_diff from erpnext.stock.utils import add_additional_uom_columns @@ -20,6 +20,11 @@ def execute(filters=None): 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")) + else: + company_currency = frappe.db.get_single_value("Global Defaults", "default_currency") + include_uom = filters.get("include_uom") columns = get_columns(filters) items = get_items(filters) @@ -52,6 +57,7 @@ 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, @@ -89,7 +95,6 @@ def execute(filters=None): def get_columns(filters): """return columns""" - columns = [ {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, @@ -97,14 +102,14 @@ def get_columns(filters): {"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}, + {"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": "Float", "width": 110}, + {"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 Value"), "fieldname": "out_val", "fieldtype": "Float", "width": 80}, - {"label": _("Valuation Rate"), "fieldname": "val_rate", "fieldtype": "Currency", "width": 90, "convertible": "rate"}, + {"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} diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index abf959eb0b..fe8ad71b73 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -4,10 +4,10 @@ from __future__ import unicode_literals import frappe +from frappe.utils import cint, flt from erpnext.stock.utils import update_included_uom_in_report from frappe import _ - def execute(filters=None): include_uom = filters.get("include_uom") columns = get_columns() @@ -15,6 +15,7 @@ def execute(filters=None): sl_entries = get_stock_ledger_entries(filters, items) item_details = get_item_details(items, sl_entries, include_uom) opening_row = get_opening_balance(filters, columns) + precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) data = [] conversion_factors = [] @@ -29,10 +30,10 @@ def execute(filters=None): sle.update(item_detail) if filters.get("batch_no"): - actual_qty += sle.actual_qty + actual_qty += flt(sle.actual_qty, precision) stock_value += sle.stock_value_difference - if sle.voucher_type == 'Stock Reconciliation': + if sle.voucher_type == 'Stock Reconciliation' and not sle.actual_qty: actual_qty = sle.qty_after_transaction stock_value = sle.stock_value 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 6a86889aa3..5873a7a300 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,7 +21,7 @@ def execute(filters=None): for cd in consumed_details.get(item_code): if (cd.voucher_no not in material_transfer_vouchers): - if cd.voucher_type=="Delivery Note": + 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": diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index f21dc3f8b0..11e758fce3 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -230,12 +230,12 @@ def get_valuation_method(item_code): def get_fifo_rate(previous_stock_queue, qty): """get FIFO (average) Rate from Queue""" - if qty >= 0: + if flt(qty) >= 0: total = sum(f[0] for f in previous_stock_queue) return sum(flt(f[0]) * flt(f[1]) for f in previous_stock_queue) / flt(total) if total else 0.0 else: available_qty_for_outgoing, outgoing_cost = 0, 0 - qty_to_pop = abs(qty) + qty_to_pop = abs(flt(qty)) while qty_to_pop and previous_stock_queue: batch = previous_stock_queue[0] if 0 < batch[0] <= qty_to_pop: diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index 596987f46a..b1ad7c8aa0 100644 --- a/erpnext/support/desk_page/support/support.json +++ b/erpnext/support/desk_page/support/support.json @@ -2,8 +2,8 @@ "cards": [ { "hidden": 0, - "label": "Service Level Agreement", - "links": "[\n {\n \"description\": \"Service Level.\",\n \"label\": \"Service Level\",\n \"name\": \"Service Level\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]" + "label": "Issues", + "links": "[\n {\n \"description\": \"Support queries from customers.\",\n \"label\": \"Issue\",\n \"name\": \"Issue\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Issue Type.\",\n \"label\": \"Issue Type\",\n \"name\": \"Issue Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Issue Priority.\",\n \"label\": \"Issue Priority\",\n \"name\": \"Issue Priority\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -12,8 +12,8 @@ }, { "hidden": 0, - "label": "Issues", - "links": "[\n {\n \"description\": \"Support queries from customers.\",\n \"label\": \"Issue\",\n \"name\": \"Issue\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Issue Type.\",\n \"label\": \"Issue Type\",\n \"name\": \"Issue Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Issue Priority.\",\n \"label\": \"Issue Priority\",\n \"name\": \"Issue Priority\",\n \"type\": \"doctype\"\n }\n]" + "label": "Service Level Agreement", + "links": "[\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -39,11 +39,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Support", - "modified": "2020-04-01 11:28:51.120583", + "modified": "2020-06-04 11:54:56.124219", "modified_by": "Administrator", "module": "Support", "name": "Support", @@ -52,19 +52,22 @@ "pin_to_top": 0, "shortcuts": [ { + "color": "#ffc4c4", + "format": "{} Assigned", "label": "Issue", "link_to": "Issue", - "type": "DocType" - }, - { - "label": "Service Level", - "link_to": "Service Level", + "stats_filter": "{\n \"_assign\": [\"like\", '%' + frappe.session.user + '%'],\n \"status\": \"Open\"\n}", "type": "DocType" }, { "label": "Maintenance Visit", "link_to": "Maintenance Visit", "type": "DocType" + }, + { + "label": "Service Level Agreement", + "link_to": "Service Level Agreement", + "type": "DocType" } ] } \ No newline at end of file diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index bad40cc37f..9e15757ce0 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -38,10 +38,35 @@ frappe.ui.form.on("Issue", { }, refresh: function (frm) { - if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") { if (frm.doc.service_level_agreement) { - set_time_to_resolve_and_response(frm); + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Service Level Agreement', + name: frm.doc.service_level_agreement + }, + callback: function(data) { + let statuses = data.message.pause_sla_on; + const hold_statuses = []; + $.each(statuses, (_i, entry) => { + hold_statuses.push(entry.status); + }); + if (hold_statuses.includes(frm.doc.status)) { + frm.dashboard.clear_headline(); + let message = {"indicator": "orange", "msg": __("SLA is on hold since {0}", [moment(frm.doc.on_hold_since).fromNow(true)])}; + frm.dashboard.set_headline_alert( + '
    ' + + '
    ' + + ''+ message.msg +' ' + + '
    ' + + '
    ' + ); + } else { + set_time_to_resolve_and_response(frm); + } + } + }); } frm.add_custom_button(__("Close"), function () { @@ -54,7 +79,8 @@ frappe.ui.form.on("Issue", { method: "erpnext.support.doctype.issue.issue.make_task", frm: frm }); - }, __("Make")); + }, __("Create")); + } else { if (frm.doc.service_level_agreement) { frm.dashboard.clear_headline(); @@ -206,4 +232,4 @@ function get_status(variance) { } else { return {"diff_display": "Failed", "indicator": "red"}; } -} \ No newline at end of file +} diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index c12cef4a5f..6525ab27d3 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -31,9 +31,13 @@ "resolution_by", "resolution_by_variance", "service_level_agreement_creation", + "on_hold_since", + "total_hold_time", "response", "mins_to_first_response", "first_responded_on", + "column_break_26", + "avg_response_time", "additional_info", "lead", "contact", @@ -50,7 +54,9 @@ "resolution_date", "content_type", "attachment", - "via_customer_portal" + "via_customer_portal", + "resolution_time", + "user_resolution_time" ], "fields": [ { @@ -114,7 +120,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Open\nReplied\nHold\nClosed", + "options": "Open\nReplied\nHold\nResolved\nClosed", "search_index": 1 }, { @@ -161,6 +167,7 @@ "options": "Service Level Agreement" }, { + "depends_on": "eval: doc.status != 'Replied';", "fieldname": "response_by", "fieldtype": "Datetime", "label": "Response By", @@ -174,6 +181,7 @@ "read_only": 1 }, { + "depends_on": "eval: doc.status != 'Replied';", "fieldname": "resolution_by", "fieldtype": "Datetime", "label": "Resolution By", @@ -328,7 +336,7 @@ "read_only": 1 }, { - "depends_on": "eval: doc.service_level_agreement", + "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", "description": "in hours", "fieldname": "response_by_variance", "fieldtype": "Float", @@ -336,7 +344,7 @@ "read_only": 1 }, { - "depends_on": "eval: doc.service_level_agreement", + "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", "description": "in hours", "fieldname": "resolution_by_variance", "fieldtype": "Float", @@ -362,12 +370,48 @@ "label": "Issue Split From", "options": "Issue", "read_only": 1 + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, + { + "bold": 1, + "fieldname": "avg_response_time", + "fieldtype": "Duration", + "label": "Average Response Time", + "read_only": 1 + }, + { + "fieldname": "resolution_time", + "fieldtype": "Duration", + "label": "Resolution Time", + "read_only": 1 + }, + { + "fieldname": "user_resolution_time", + "fieldtype": "Duration", + "label": "User Resolution Time", + "read_only": 1 + }, + { + "fieldname": "on_hold_since", + "fieldtype": "Datetime", + "hidden": 1, + "label": "On Hold Since", + "read_only": 1 + }, + { + "fieldname": "total_hold_time", + "fieldtype": "Duration", + "label": "Total Hold Time", + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-03-13 02:19:49.477928", + "modified": "2020-06-10 12:47:37.146914", "modified_by": "Administrator", "module": "Support", "name": "Issue", @@ -395,4 +439,4 @@ "title_field": "subject", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 117267f1a4..87168e151e 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -7,7 +7,7 @@ import json from frappe import _ from frappe import utils from frappe.model.document import Document -from frappe.utils import now, time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, today, get_time, get_datetime +from frappe.utils import time_diff_in_hours, now_datetime, getdate, get_weekdays, add_to_date, today, get_time, get_datetime, time_diff_in_seconds, time_diff from datetime import datetime, timedelta from frappe.model.mapper import get_mapped_doc from frappe.utils.user import is_website_user @@ -47,8 +47,8 @@ class Issue(Document): self.contact = frappe.db.get_value("Contact", {"email_id": email_id}) if self.contact: - contact = frappe.get_doc('Contact', self.contact) - self.customer = contact.get_link_for('Customer') + contact = frappe.get_doc("Contact", self.contact) + self.customer = contact.get_link_for("Customer") if not self.company: self.company = frappe.db.get_value("Lead", self.lead, "company") or \ @@ -56,18 +56,75 @@ class Issue(Document): def update_status(self): status = frappe.db.get_value("Issue", self.name, "status") - if self.status!="Open" and status =="Open" and not self.first_responded_on: + if self.status != "Open" and status == "Open" and not self.first_responded_on: self.first_responded_on = frappe.flags.current_time or now_datetime() - if self.status=="Closed" and status !="Closed": + if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]: self.resolution_date = frappe.flags.current_time or now_datetime() if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing": set_service_level_agreement_variance(issue=self.name) self.update_agreement_status() + set_resolution_time(issue=self) + set_user_resolution_time(issue=self) - if self.status=="Open" and status !="Open": + if self.status == "Open" and status != "Open": # if no date, it should be set as None and not a blank string "", as per mysql strict config self.resolution_date = None + self.reset_issue_metrics() + # enable SLA and variance on Reopen + self.agreement_fulfilled = "Ongoing" + set_service_level_agreement_variance(issue=self.name) + + self.handle_hold_time(status) + + 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}) + 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() + 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 + + # 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: + hold_time = self.total_hold_time if self.total_hold_time else 0 + now_time = frappe.flags.current_time or now_datetime() + last_hold_time = 0 + 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 + + # re-calculate SLA variables after issue changes from any hold status to any non-hold status + # add hold time to SLA variables + start_date_time = get_datetime(self.service_level_agreement_creation) + priority = get_priority(self) + 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 = add_to_date(response_by, seconds=round(last_hold_time)) + response_by_variance = round(time_diff_in_hours(response_by, now_time)) + update_values['response_by'] = response_by + update_values['response_by_variance'] = response_by_variance + (last_hold_time // 3600) + + 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_hours(resolution_by, now_time)) + update_values['resolution_by'] = resolution_by + update_values['resolution_by_variance'] = resolution_by_variance + (last_hold_time // 3600) + update_values['on_hold_since'] = None + + self.db_set(update_values) def update_agreement_status(self): if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": @@ -128,6 +185,7 @@ class Issue(Document): replicated_issue.response_by_variance = None replicated_issue.resolution_by = None replicated_issue.resolution_by_variance = None + replicated_issue.reset_issue_metrics() frappe.get_doc(replicated_issue).insert() @@ -137,7 +195,7 @@ class Issue(Document): 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) @@ -173,20 +231,15 @@ class Issue(Document): self.service_level_agreement = service_level_agreement.name self.priority = service_level_agreement.default_priority if not priority else priority - service_level_agreement = frappe.get_doc("Service Level Agreement", service_level_agreement.name) - priority = service_level_agreement.get_service_level_agreement_priority(self.priority) - priority.update({ - "support_and_resolution": service_level_agreement.support_and_resolution, - "holiday_list": service_level_agreement.holiday_list - }) + priority = get_priority(self) if not self.creation: self.creation = now_datetime() 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_hours(self.response_by, now_datetime())) self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime())) @@ -221,36 +274,41 @@ class Issue(Document): self.agreement_fulfilled = "Ongoing" self.save() + def reset_issue_metrics(self): + self.db_set("resolution_time", None) + self.db_set("user_resolution_time", None) + + +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 + }) + return priority + + def get_expected_time_for(parameter, service_level, start_date_time): current_date_time = start_date_time expected_time = current_date_time start_time = None end_time = None - # lets assume response time is in days by default - if parameter == 'response': - allotted_days = service_level.get("response_time") - time_period = service_level.get("response_time_period") - elif parameter == 'resolution': - allotted_days = service_level.get("resolution_time") - time_period = service_level.get("resolution_time_period") + if parameter == "response": + allotted_seconds = service_level.get("response_time") + elif parameter == "resolution": + allotted_seconds = service_level.get("resolution_time") else: frappe.throw(_("{0} parameter is invalid").format(parameter)) - allotted_hours = 0 - if time_period == 'Hour': - allotted_hours = allotted_days - allotted_days = 0 - elif time_period == 'Week': - allotted_days *= 7 - - expected_time_is_set = 1 if allotted_days == 0 and time_period in ['Day', 'Week'] else 0 + expected_time_is_set = 0 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, + "start_time": service.start_time, + "end_time": service.end_time, }) holidays = get_holidays(service_level.get("holiday_list")) @@ -264,25 +322,22 @@ def get_expected_time_for(parameter, service_level, start_date_time): 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_hours(end_time, start_time) + time_left_today = time_diff_in_seconds(end_time, start_time) # no time left for support today - if time_left_today < 0: pass - elif time_period == 'Hour': - if time_left_today >= allotted_hours: + 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)) - expected_time = add_to_date(expected_time, hours=allotted_hours) + expected_time = add_to_date(expected_time, seconds=allotted_seconds) expected_time_is_set = 1 else: - allotted_hours = allotted_hours - time_left_today - else: - allotted_days -= 1 - expected_time_is_set = allotted_days <= 0 + allotted_seconds = allotted_seconds - time_left_today if not expected_time_is_set: current_date_time = add_to_date(current_date_time, days=1) - if end_time and time_period != 'Hour': + if end_time and allotted_seconds >= 86400: current_date_time = datetime.combine(getdate(current_date_time), get_time(end_time)) else: current_date_time = expected_time @@ -311,6 +366,36 @@ def set_service_level_agreement_variance(issue=None): if variance < 0: frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) + +def set_resolution_time(issue): + # total time taken from issue creation to closing + resolution_time = time_diff_in_seconds(issue.resolution_date, issue.creation) + issue.db_set("resolution_time", resolution_time) + + +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 + }, + fields=["sent_or_received", "name", "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 wait_time > 0: + pending_time.append(wait_time) + + total_pending_time = sum(pending_time) + resolution_time_in_secs = time_diff_in_seconds(issue.resolution_date, issue.creation) + user_resolution_time = resolution_time_in_secs - total_pending_time + issue.db_set("user_resolution_time", user_resolution_time) + + def get_list_context(context=None): return { "title": _("Issues"), @@ -318,7 +403,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 } @@ -326,12 +411,12 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord from frappe.www.list import get_list user = frappe.session.user - contact = frappe.db.get_value('Contact', {'user': user}, 'name') + contact = frappe.db.get_value("Contact", {"user": user}, "name") customer = None if contact: - contact_doc = frappe.get_doc('Contact', contact) - customer = contact_doc.get_link_for('Customer') + contact_doc = frappe.get_doc("Contact", contact) + customer = contact_doc.get_link_for("Customer") ignore_permissions = False if is_website_user(): diff --git a/erpnext/support/doctype/issue/issue_list.js b/erpnext/support/doctype/issue/issue_list.js index 6d702f6bdf..513a8dca22 100644 --- a/erpnext/support/doctype/issue/issue_list.js +++ b/erpnext/support/doctype/issue/issue_list.js @@ -8,11 +8,11 @@ frappe.listview_settings['Issue'] = { var method = "erpnext.support.doctype.issue.issue.set_multiple_status"; - listview.page.add_menu_item(__("Set as Open"), function() { + listview.page.add_action_item(__("Set as Open"), function() { listview.call_for_selected_items(method, {"status": "Open"}); }); - listview.page.add_menu_item(__("Set as Closed"), function() { + listview.page.add_action_item(__("Set as Closed"), function() { listview.call_for_selected_items(method, {"status": "Closed"}); }); }, diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 7a5e3e300d..fb8ceb53b2 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -5,15 +5,18 @@ from __future__ import unicode_literals import frappe import unittest from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues -from frappe.utils import now_datetime, get_datetime +from frappe.utils import now_datetime, get_datetime, flt import datetime from datetime import timedelta class TestIssue(unittest.TestCase): - def test_response_time_and_resolution_time_based_on_different_sla(self): + def setUp(self): + frappe.db.sql("delete from `tabService Level Agreement`") + frappe.db.sql("delete from `tabEmployee`") frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) create_service_level_agreements_for_issues() + def test_response_time_and_resolution_time_based_on_different_sla(self): creation = datetime.datetime(2019, 3, 4, 12, 0) # make issue with customer specific SLA @@ -72,8 +75,67 @@ class TestIssue(unittest.TestCase): self.assertEqual(issue.agreement_fulfilled, 'Fulfilled') -def make_issue(creation=None, customer=None, index=0): + def test_issue_metrics(self): + creation = datetime.datetime(2020, 3, 4, 4, 0) + issue = make_issue(creation, index=1) + create_communication(issue.name, "test@example.com", "Received", creation) + + creation = datetime.datetime(2020, 3, 4, 4, 15) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + creation = datetime.datetime(2020, 3, 4, 5, 0) + create_communication(issue.name, "test@example.com", "Received", creation) + + creation = datetime.datetime(2020, 3, 4, 5, 5) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5) + issue.reload() + issue.status = 'Closed' + issue.save() + + self.assertEqual(issue.avg_response_time, 600) + self.assertEqual(issue.resolution_time, 3900) + self.assertEqual(issue.user_resolution_time, 1200) + + def test_hold_time_on_replied(self): + creation = datetime.datetime(2020, 3, 4, 4, 0) + + issue = make_issue(creation, index=1) + create_communication(issue.name, "test@example.com", "Received", creation) + + creation = datetime.datetime(2020, 3, 4, 4, 15) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15) + issue.reload() + issue.status = 'Replied' + issue.save() + + self.assertEqual(issue.on_hold_since, frappe.flags.current_time) + + creation = datetime.datetime(2020, 3, 4, 5, 0) + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 0) + create_communication(issue.name, "test@example.com", "Received", creation) + + issue.reload() + self.assertEqual(flt(issue.total_hold_time, 2), 2700) + self.assertEqual(issue.resolution_by, datetime.datetime(2020, 3, 4, 16, 45)) + + creation = datetime.datetime(2020, 3, 4, 5, 5) + create_communication(issue.name, "test@admin.com", "Sent", creation) + + frappe.flags.current_time = datetime.datetime(2020, 3, 4, 5, 5) + issue.reload() + issue.status = 'Closed' + issue.save() + + issue.reload() + self.assertEqual(flt(issue.total_hold_time, 2), 2700) + + +def make_issue(creation=None, customer=None, index=0): issue = frappe.get_doc({ "doctype": "Issue", "subject": "Service Level Agreement Issue {0}".format(index), @@ -86,6 +148,7 @@ def make_issue(creation=None, customer=None, index=0): return issue + def create_customer(name, customer_group, territory): create_customer_group(customer_group) @@ -99,6 +162,7 @@ def create_customer(name, customer_group, territory): "territory": territory }).insert(ignore_permissions=True) + def create_customer_group(customer_group): if not frappe.db.exists("Customer Group", {"customer_group_name": customer_group}): @@ -107,6 +171,7 @@ def create_customer_group(customer_group): "customer_group_name": customer_group }).insert(ignore_permissions=True) + def create_territory(territory): if not frappe.db.exists("Territory", {"territory_name": territory}): @@ -114,3 +179,21 @@ def create_territory(territory): "doctype": "Territory", "territory_name": territory, }).insert(ignore_permissions=True) + + +def create_communication(reference_name, sender, sent_or_received, creation): + issue = frappe.get_doc({ + "doctype": "Communication", + "communication_type": "Communication", + "communication_medium": "Email", + "sent_or_received": sent_or_received, + "email_status": "Open", + "subject": "Test Issue", + "sender": sender, + "content": "Test", + "status": "Linked", + "reference_doctype": "Issue", + "creation": creation, + "reference_name": reference_name + }) + issue.save() diff --git a/erpnext/support/doctype/pause_sla_on_status/__init__.py b/erpnext/support/doctype/pause_sla_on_status/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json new file mode 100644 index 0000000000..5b03f25f48 --- /dev/null +++ b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2020-06-05 13:59:43.265588", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "status" + ], + "fields": [ + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-06-05 15:15:29.986608", + "modified_by": "Administrator", + "module": "Support", + "name": "Pause SLA On Status", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py new file mode 100644 index 0000000000..a3b547e480 --- /dev/null +++ b/erpnext/support/doctype/pause_sla_on_status/pause_sla_on_status.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class PauseSLAOnStatus(Document): + pass diff --git a/erpnext/support/doctype/service_level/service_level.js b/erpnext/support/doctype/service_level/service_level.js deleted file mode 100644 index abe254bd03..0000000000 --- a/erpnext/support/doctype/service_level/service_level.js +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Service Level', { - -}); diff --git a/erpnext/support/doctype/service_level/service_level.json b/erpnext/support/doctype/service_level/service_level.json deleted file mode 100644 index dced3aa9e9..0000000000 --- a/erpnext/support/doctype/service_level/service_level.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "autoname": "field:service_level", - "creation": "2018-11-19 12:44:30.407502", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "service_level", - "employee_group", - "column_break_2", - "holiday_list", - "default_priority", - "response_and_resoution_time", - "priorities", - "section_break_01", - "support_and_resolution" - ], - "fields": [ - { - "fieldname": "service_level", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Level", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "holiday_list", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Holiday List (ignored during SLA calculation)", - "options": "Holiday List", - "reqd": 1 - }, - { - "fieldname": "employee_group", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Employee Group", - "options": "Employee Group" - }, - { - "fieldname": "response_and_resoution_time", - "fieldtype": "Section Break", - "label": "Response and Resoution Time" - }, - { - "fieldname": "section_break_01", - "fieldtype": "Section Break", - "label": "Support Hours" - }, - { - "fieldname": "support_and_resolution", - "fieldtype": "Table", - "label": "Support and Resolution", - "options": "Service Day", - "reqd": 1 - }, - { - "fieldname": "priorities", - "fieldtype": "Table", - "label": "Priorities", - "options": "Service Level Priority", - "reqd": 1 - }, - { - "fieldname": "default_priority", - "fieldtype": "Link", - "label": "Default Priority", - "options": "Issue Priority", - "read_only": 1 - } - ], - "modified": "2019-06-06 12:58:03.464056", - "modified_by": "Administrator", - "module": "Support", - "name": "Service Level", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "All", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC" -} \ No newline at end of file diff --git a/erpnext/support/doctype/service_level/service_level.py b/erpnext/support/doctype/service_level/service_level.py deleted file mode 100644 index 89fa25c233..0000000000 --- a/erpnext/support/doctype/service_level/service_level.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -from frappe.model.document import Document -from datetime import datetime -from frappe.utils import get_weekdays - -class ServiceLevel(Document): - - def validate(self): - self.check_priorities() - self.check_support_and_resolution() - - def check_priorities(self): - default_priority = [] - priorities = [] - - for priority in self.priorities: - # Check if response and resolution time is set for every priority - if not (priority.response_time or priority.resolution_time): - frappe.throw(_("Set Response Time and Resolution for Priority {0} at index {1}.").format(priority.priority, priority.idx)) - - priorities.append(priority.priority) - - if priority.default_priority: - default_priority.append(priority.default_priority) - - if priority.response_time_period == "Hour": - response = priority.response_time * 0.0416667 - elif priority.response_time_period == "Day": - response = priority.response_time - elif priority.response_time_period == "Week": - response = priority.response_time * 7 - - if priority.resolution_time_period == "Hour": - resolution = priority.resolution_time * 0.0416667 - elif priority.resolution_time_period == "Day": - resolution = priority.resolution_time - elif priority.resolution_time_period == "Week": - resolution = priority.resolution_time * 7 - - if response > resolution: - frappe.throw(_("Response Time for {0} at index {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx)) - - # Check if repeated priority - if not len(set(priorities)) == len(priorities): - repeated_priority = get_repeated(priorities) - frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority)) - - # Check if repeated default priority - if not len(set(default_priority)) == len(default_priority): - frappe.throw(_("Select only one Priority as Default.")) - - # set default priority from priorities - try: - self.default_priority = next(d.priority for d in self.priorities if d.default_priority) - except Exception: - frappe.throw(_("Select a Default Priority.")) - - def check_support_and_resolution(self): - week = get_weekdays() - support_days = [] - - 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))) - - 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))) - - # Check for repeated workday - if not len(set(support_days)) == len(support_days): - repeated_days = get_repeated(support_days) - frappe.throw(_("Workday {0} has been repeated.").format(repeated_days)) - -def get_repeated(values): - unique_list = [] - diff = [] - for value in values: - if value not in unique_list: - unique_list.append(str(value)) - else: - if value not in diff: - diff.append(str(value)) - return " ".join(diff) diff --git a/erpnext/support/doctype/service_level/service_level_dashboard.py b/erpnext/support/doctype/service_level/service_level_dashboard.py deleted file mode 100644 index 393095e117..0000000000 --- a/erpnext/support/doctype/service_level/service_level_dashboard.py +++ /dev/null @@ -1,12 +0,0 @@ -from frappe import _ - -def get_data(): - return { - 'fieldname': 'service_level', - 'transactions': [ - { - 'label': _('Service Level Agreement'), - 'items': ['Service Level Agreement'] - } - ] - } \ No newline at end of file diff --git a/erpnext/support/doctype/service_level/test_service_level.py b/erpnext/support/doctype/service_level/test_service_level.py deleted file mode 100644 index 09577df166..0000000000 --- a/erpnext/support/doctype/service_level/test_service_level.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals -from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group -from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities - -import frappe -import unittest - -class TestServiceLevel(unittest.TestCase): - - def test_service_level(self): - employee_group = make_employee_group() - make_holiday_list() - make_priorities() - - # Default Service Level - test_make_service_level = create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6) - get_make_service_level = get_service_level("__Test Service Level") - - self.assertEqual(test_make_service_level.name, get_make_service_level.name) - self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list) - self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group) - - # Service Level - test_make_service_level = create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3) - get_make_service_level = get_service_level("_Test Service Level") - - self.assertEqual(test_make_service_level.name, get_make_service_level.name) - self.assertEqual(test_make_service_level.holiday_list, get_make_service_level.holiday_list) - self.assertEqual(test_make_service_level.employee_group, get_make_service_level.employee_group) - - -def create_service_level(service_level, holiday_list, employee_group, response_time, resolution_time): - sl = frappe.get_doc({ - "doctype": "Service Level", - "service_level": service_level, - "holiday_list": holiday_list, - "employee_group": employee_group, - "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", - } - ], - "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", - }, - { - "workday": "Saturday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Sunday", - "start_time": "10:00:00", - "end_time": "18:00:00", - } - ] - }) - - sl_exists = frappe.db.exists("Service Level", {"service_level": service_level}) - - if not sl_exists: - sl.insert() - return sl - else: - return frappe.get_doc("Service Level", {"service_level": service_level}) - -def get_service_level(service_level): - return frappe.get_doc("Service Level", service_level) - -def make_holiday_list(): - holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") - if not holiday_list: - now = frappe.utils.now_datetime() - 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() - -def create_service_level_for_sla(): - employee_group = make_employee_group() - make_holiday_list() - make_priorities() - - # Default Service Level - create_service_level("__Test Service Level", "__Test Holiday List", employee_group, 4, 6) - - # Service Level - create_service_level("_Test Service Level", "__Test Holiday List", employee_group, 2, 3) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index 1d486f4834..5346195a39 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -2,28 +2,15 @@ // For license information, please see license.txt frappe.ui.form.on('Service Level Agreement', { - service_level: function(frm) { - frm.fields_dict.support_and_resolution.grid.remove_all(); - frappe.call({ - "method": "frappe.client.get", - args: { - doctype: "Service Level", - name: frm.doc.service_level - }, - callback: function(data){ - let count = Math.max(data.message.priorities.length, data.message.support_and_resolution.length); - let i = 0; - while (i < count){ - if (data.message.priorities[i]) { - frm.add_child("priorities", data.message.priorities[i]); - } - if (data.message.support_and_resolution[i]) { - frm.add_child("support_and_resolution", data.message.support_and_resolution[i]); - } - i++; - } - frm.refresh(); - } + setup: function(frm) { + let allow_statuses = []; + const exclude_statuses = ['Open', 'Closed', 'Resolved']; + + frappe.model.with_doctype('Issue', () => { + let statuses = frappe.meta.get_docfield('Issue', 'status', frm.doc.name).options; + statuses = statuses.split('\n'); + allow_statuses = statuses.filter((status) => !exclude_statuses.includes(status)); + frappe.meta.get_docfield('Pause SLA On Status', 'status', frm.doc.name).options = [''].concat(allow_statuses); }); - }, -}); + } +}); \ No newline at end of file diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 9a83ca7ac0..939c199982 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "format:SLA-{service_level}-{####}", "creation": "2018-12-26 21:08:15.448812", "doctype": "DocType", @@ -6,12 +7,13 @@ "engine": "InnoDB", "field_order": [ "enable", + "section_break_2", "service_level", + "default_priority", "default_service_level_agreement", - "holiday_list", "column_break_2", "employee_group", - "default_priority", + "holiday_list", "entity_section", "entity_type", "column_break_10", @@ -21,49 +23,40 @@ "active", "column_break_7", "end_date", + "section_break_18", + "pause_sla_on", "response_and_resolution_time_section", "priorities", "support_and_resolution_section_break", "support_and_resolution" ], "fields": [ - { - "default": "0", - "depends_on": "eval: !doc.customer;", - "fieldname": "default_service_level_agreement", - "fieldtype": "Check", - "label": "Default Service Level Agreement" - }, { "fieldname": "service_level", - "fieldtype": "Link", + "fieldtype": "Data", "in_list_view": 1, "in_standard_filter": 1, "label": "Service Level", - "options": "Service Level", "reqd": 1 }, { - "fetch_from": "service_level.holiday_list", "fieldname": "holiday_list", "fieldtype": "Link", "label": "Holiday List", "options": "Holiday List", - "read_only": 1 + "reqd": 1 }, { "fieldname": "column_break_2", "fieldtype": "Column Break" }, { - "fetch_from": "service_level.employee_group", "fieldname": "employee_group", "fieldtype": "Link", "in_list_view": 1, "in_standard_filter": 1, "label": "Employee Group", - "options": "Employee Group", - "read_only": 1 + "options": "Employee Group" }, { "fieldname": "agreement_details_section", @@ -103,21 +96,15 @@ "fieldname": "support_and_resolution", "fieldtype": "Table", "label": "Support and Resolution", - "options": "Service Day" + "options": "Service Day", + "reqd": 1 }, { "fieldname": "priorities", "fieldtype": "Table", "label": "Priorities", - "options": "Service Level Priority" - }, - { - "fetch_from": "service_level.default_priority", - "fieldname": "default_priority", - "fieldtype": "Link", - "label": "Default Priority", - "options": "Issue Priority", - "read_only": 1 + "options": "Service Level Priority", + "reqd": 1 }, { "default": "1", @@ -156,9 +143,38 @@ "fieldname": "enable", "fieldtype": "Check", "label": "Enable" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "default_service_level_agreement", + "fieldtype": "Check", + "label": "Default Service Level Agreement" + }, + { + "fieldname": "default_priority", + "fieldtype": "Link", + "label": "Default Priority", + "options": "Issue Priority", + "read_only": 1 + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "pause_sla_on", + "fieldtype": "Table", + "label": "Pause SLA On", + "options": "Pause SLA On Status" } ], - "modified": "2019-07-09 17:22:16.402939", + "links": [], + "modified": "2020-06-10 12:30:15.050785", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index a399c58b16..70c469663b 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -6,13 +6,76 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe import _ -from frappe.utils import getdate +from frappe.utils import getdate, get_weekdays, get_link_to_form class ServiceLevelAgreement(Document): def validate(self): - if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): - frappe.throw(_("Service Level Agreement tracking is not enabled.")) + self.validate_doc() + self.check_priorities() + self.check_support_and_resolution() + + def check_priorities(self): + default_priority = [] + priorities = [] + + for priority in self.priorities: + # Check if response and resolution time is set for every priority + if not priority.response_time or not priority.resolution_time: + frappe.throw(_("Set Response Time and Resolution Time for Priority {0} in row {1}.").format(priority.priority, priority.idx)) + + priorities.append(priority.priority) + + if priority.default_priority: + default_priority.append(priority.default_priority) + + response = priority.response_time + resolution = priority.resolution_time + + if response > resolution: + 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): + repeated_priority = get_repeated(priorities) + frappe.throw(_("Priority {0} has been repeated.").format(repeated_priority)) + + # Check if repeated default priority + if not len(set(default_priority)) == len(default_priority): + frappe.throw(_("Select only one Priority as Default.")) + + # set default priority from priorities + try: + self.default_priority = next(d.priority for d in self.priorities if d.default_priority) + except Exception: + frappe.throw(_("Select a Default Priority.")) + + def check_support_and_resolution(self): + week = get_weekdays() + support_days = [] + + 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))) + + 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))) + + # Check for repeated workday + if not len(set(support_days)) == len(support_days): + repeated_days = get_repeated(support_days) + 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 self.default_service_level_agreement: if frappe.db.exists("Service Level Agreement", {"default_service_level_agreement": "1", "name": ["!=", self.name]}): @@ -35,9 +98,7 @@ class ServiceLevelAgreement(Document): return frappe._dict({ "priority": priority.priority, "response_time": priority.response_time, - "response_time_period": priority.response_time_period, - "resolution_time": priority.resolution_time, - "resolution_time_period": priority.resolution_time_period + "resolution_time": priority.resolution_time }) def check_agreement_status(): @@ -110,4 +171,15 @@ def get_service_level_agreement_filters(name, customer=None): 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)] - } \ No newline at end of file + } + +def get_repeated(values): + unique_list = [] + diff = [] + for value in values: + if value not in unique_list: + unique_list.append(str(value)) + else: + if value not in diff: + diff.append(str(value)) + return " ".join(diff) 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 4a741ea4e1..07ef368cbe 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 @@ -5,19 +5,20 @@ from __future__ import unicode_literals import frappe import unittest -from erpnext.support.doctype.service_level.test_service_level import create_service_level_for_sla +from erpnext.hr.doctype.employee_group.test_employee_group import make_employee_group +from erpnext.support.doctype.issue_priority.test_issue_priority import make_priorities class TestServiceLevelAgreement(unittest.TestCase): - - def test_service_level_agreement(self): + def setUp(self): + frappe.db.sql("delete from `tabService Level Agreement`") frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) - create_service_level_for_sla() - + def test_service_level_agreement(self): # Default Service Level Agreement create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1, - service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type=None, entity=None, response_time=4, resolution_time=6) + 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) self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name) @@ -28,8 +29,8 @@ class TestServiceLevelAgreement(unittest.TestCase): # Service Level Agreement for Customer customer = create_customer() create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer", entity=customer, response_time=2, resolution_time=3) + 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) @@ -40,8 +41,8 @@ class TestServiceLevelAgreement(unittest.TestCase): # 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, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer Group", entity=customer_group, response_time=2, resolution_time=3) + 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) @@ -52,8 +53,8 @@ class TestServiceLevelAgreement(unittest.TestCase): # Service Level Agreement for Territory territory = create_territory() create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Territory", entity=territory, response_time=2, resolution_time=3) + 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) @@ -71,14 +72,19 @@ 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, service_level, holiday_list, employee_group, +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, - "service_level": service_level, + "default_priority": "Medium", "holiday_list": holiday_list, "employee_group": employee_group, "entity_type": entity_type, @@ -109,6 +115,11 @@ def create_service_level_agreement(default_service_level_agreement, service_leve "resolution_time_period": "Hour", } ], + "pause_sla_on": [ + { + "status": "Replied" + } + ], "support_and_resolution": [ { "workday": "Monday", @@ -167,6 +178,7 @@ def create_service_level_agreement(default_service_level_agreement, service_leve else: return frappe.get_doc("Service Level Agreement", service_level_agreement_exists) + def create_customer(): customer = frappe.get_doc({ "doctype": "Customer", @@ -206,23 +218,41 @@ def create_territory(): return frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}) def create_service_level_agreements_for_issues(): - create_service_level_for_sla() - - create_service_level_agreement(default_service_level_agreement=1, - service_level="__Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type=None, entity=None, response_time=4, resolution_time=6) + 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, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer", entity="_Test Customer", response_time=2, resolution_time=3) + 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, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=2, resolution_time=3) + 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, - service_level="_Test Service Level", holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Territory", entity="_Test SLA Territory", response_time=2, resolution_time=3) + 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() diff --git a/erpnext/support/doctype/service_level_priority/service_level_priority.json b/erpnext/support/doctype/service_level_priority/service_level_priority.json index cd87a1c113..65d51694cc 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-05-04 05:54:03.658991", "doctype": "DocType", "editable_grid": 1, @@ -9,10 +10,8 @@ "default_priority", "sb_00", "response_time", - "response_time_period", "cb_00", - "resolution_time", - "resolution_time_period" + "resolution_time" ], "fields": [ { @@ -28,16 +27,11 @@ "fieldtype": "Section Break" }, { - "columns": 1, - "fieldname": "response_time", - "fieldtype": "Int", - "in_list_view": 1, - "label": "Response Time" - }, - { - "columns": 1, + "columns": 2, "fieldname": "resolution_time", - "fieldtype": "Int", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Resolution Time" }, @@ -45,36 +39,31 @@ "fieldname": "cb_00", "fieldtype": "Column Break" }, - { - "columns": 2, - "fieldname": "response_time_period", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Response Time Period", - "options": "Hour\nDay\nWeek" - }, - { - "columns": 2, - "fieldname": "resolution_time_period", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Resolution Time Period", - "options": "Hour\nDay\nWeek" - }, { "fieldname": "cb_01", "fieldtype": "Column Break" }, { + "columns": 1, "default": "0", "fieldname": "default_priority", "fieldtype": "Check", "in_list_view": 1, "label": "Default Priority" + }, + { + "columns": 2, + "fieldname": "response_time", + "fieldtype": "Duration", + "hide_days": 1, + "hide_seconds": 1, + "in_list_view": 1, + "label": "First Response Time" } ], "istable": 1, - "modified": "2019-05-21 06:54:42.674377", + "links": [], + "modified": "2020-06-10 12:45:47.545915", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", @@ -84,4 +73,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/support/doctype/support_settings/support_settings.js b/erpnext/support/doctype/support_settings/support_settings.js index 1d1069d58b..78adca81ca 100644 --- a/erpnext/support/doctype/support_settings/support_settings.js +++ b/erpnext/support/doctype/support_settings/support_settings.js @@ -3,6 +3,6 @@ frappe.ui.form.on('Support Settings', { refresh: function(frm) { - + // } }); diff --git a/erpnext/support/doctype/support_settings/support_settings.json b/erpnext/support/doctype/support_settings/support_settings.json index be9e064591..5d3d3ace59 100644 --- a/erpnext/support/doctype/support_settings/support_settings.json +++ b/erpnext/support/doctype/support_settings/support_settings.json @@ -1,4 +1,5 @@ { + "actions": "", "creation": "2017-02-17 13:07:35.686409", "doctype": "DocType", "editable_grid": 1, @@ -21,6 +22,10 @@ "post_description_key", "post_route_key", "post_route_string", + "greetings_section_section", + "greeting_title", + "column_break_19", + "greeting_subtitle", "search_apis_sb", "search_apis" ], @@ -122,13 +127,44 @@ }, { "default": "0", + "depends_on": "eval:doc.track_service_level_agreement;", "fieldname": "allow_resetting_service_level_agreement", "fieldtype": "Check", "label": "Allow Resetting Service Level Agreement" + }, + { + "default": "We're here to help", + "fieldname": "greeting_title", + "fieldtype": "Data", + "label": "Greeting Title", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "Browse help topics", + "fieldname": "greeting_subtitle", + "fieldtype": "Data", + "label": "Greeting Subtitle", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "greetings_section_section", + "fieldtype": "Section Break", + "label": "Greetings Section", + "show_days": 1, + "show_seconds": 1 } ], "issingle": 1, - "modified": "2019-07-10 22:52:39.663873", + "links": [], + "modified": "2020-06-11 13:08:38.473616", "modified_by": "Administrator", "module": "Support", "name": "Support Settings", diff --git a/erpnext/support/web_form/issues/issues.json b/erpnext/support/web_form/issues/issues.json index 0f15e4737f..1df9fb7969 100644 --- a/erpnext/support/web_form/issues/issues.json +++ b/erpnext/support/web_form/issues/issues.json @@ -18,7 +18,7 @@ "is_standard": 1, "login_required": 1, "max_attachment_size": 0, - "modified": "2020-03-06 05:24:05.749664", + "modified": "2020-05-19 13:01:10.729088", "modified_by": "Administrator", "module": "Support", "name": "issues", @@ -76,7 +76,7 @@ { "allow_read_on_all_link_options": 0, "fieldname": "description", - "fieldtype": "Text", + "fieldtype": "Text Editor", "hidden": 0, "label": "Description", "max_length": 0, diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js index 5fd901169f..163c955c56 100644 --- a/erpnext/templates/generators/item/item_configure.js +++ b/erpnext/templates/generators/item/item_configure.js @@ -193,14 +193,17 @@ class ItemConfigure { filtered_items_count === 1 ? filtered_items[0] : ''; + // Allow Add to Cart if adding out of stock items enabled in Shopping Cart else check stock. + const in_stock = product_info.allow_items_not_in_stock ? 1 : product_info.in_stock; + const add_to_cart = `${__('Add to cart')}`; + const product_action = in_stock ? add_to_cart : `${__('Not in Stock')}`; + const item_add_to_cart = one_item ? ` `: ''; diff --git a/erpnext/templates/generators/student_admission.html b/erpnext/templates/generators/student_admission.html index ae70df8b08..8b153448ee 100644 --- a/erpnext/templates/generators/student_admission.html +++ b/erpnext/templates/generators/student_admission.html @@ -14,12 +14,12 @@ {%- if introduction -%}
    {{ introduction }}
    -{% endif %} +{% endif %} -{%- if application_form_route -%} +{%- if doc.enable_admission_application -%}

    + href='/student-applicant'> {{ _("Apply Now") }}

    {% endif %} diff --git a/erpnext/templates/includes/cart/address_card.html b/erpnext/templates/includes/cart/address_card.html index c91723e91e..646210e65f 100644 --- a/erpnext/templates/includes/cart/address_card.html +++ b/erpnext/templates/includes/cart/address_card.html @@ -3,7 +3,7 @@
    -
    {{ address.name }}
    +
    {{ address.title }}

    {{ address.display }}

    diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index 60de3af17b..aa25c885fe 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -109,7 +109,7 @@ frappe.ready(() => { reqd: 1 }, { - label: __('Pin Code'), + label: __('Postal Code'), fieldname: 'pincode', fieldtype: 'Data' }, diff --git a/erpnext/templates/includes/footer/footer_powered.html b/erpnext/templates/includes/footer/footer_powered.html index cf7661ee3f..82b2716a92 100644 --- a/erpnext/templates/includes/footer/footer_powered.html +++ b/erpnext/templates/includes/footer/footer_powered.html @@ -10,9 +10,18 @@ 'Agriculture': '/agriculture', 'Hospitality': '' } %} + {% set link = '' %} +{% set label = '' %} {% if domains %} - {% set link = links[domains[0].domain] %} + {% set label = domains[0].domain %} + {% set link = links[label] %} {% endif %} -Powered by ERPNext - {{ '' if domains else 'Open Source' }} ERP Software {{ ('for ' + domains[0].domain + ' Companies') if domains else '' }} +{% if label == "Services" %} + {% set label = "Service" %} +{% endif %} + + + +Powered by ERPNext - {{ '' if domains else 'Open Source' }} ERP Software {{ ('for ' + label + ' Companies') if domains else '' }} diff --git a/erpnext/tests/test_woocommerce.py b/erpnext/tests/test_woocommerce.py index ce0f47d685..df715ab202 100644 --- a/erpnext/tests/test_woocommerce.py +++ b/erpnext/tests/test_woocommerce.py @@ -24,7 +24,7 @@ class TestWoocommerce(unittest.TestCase): woo_settings.creation_user = "Administrator" woo_settings.save(ignore_permissions=True) - def test_sales_order_for_woocommerece(self): + def test_sales_order_for_woocommerce(self): frappe.flags.woocomm_test_order_data = {"id":75,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":False,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"Woocommerce","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":"","date_paid_gmt":"","date_completed":"","date_completed_gmt":"","cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]} order() diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 7960941e9e..b0fbf37a59 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -18,9 +18,9 @@ 90-Above,90-Dessus, A Customer Group exists with same name please change the Customer name or rename the Customer Group,"Un Groupe de Clients existe avec le même nom, veuillez changer le nom du Client ou renommer le Groupe de Clients", A Default Service Level Agreement already exists.,Un accord de niveau de service par défaut existe déjà., -A Lead requires either a person's name or an organization's name,Un responsable requiert le nom d'une personne ou le nom d'une organisation, +A Lead requires either a person's name or an organization's name,Un responsable requiert le nom d'une personne ou le nom d'une organisation, A customer with the same name already exists,Un client avec un nom identique existe déjà, -A question must have more than one options,Une question doit avoir plus d'une option, +A question must have more than one options,Une question doit avoir plus d'une option, A qustion must have at least one correct options,Une qustion doit avoir au moins une des options correctes, A {0} exists between {1} and {2} (,Un {0} existe entre {1} et {2} (, A4,A4, @@ -30,7 +30,7 @@ Abbr can not be blank or space,Abré. ne peut être vide ou contenir un espace, Abbreviation already used for another company,Abréviation déjà utilisée pour une autre société, Abbreviation cannot have more than 5 characters,L'abbréviation ne peut pas avoir plus de 5 caractères, Abbreviation is mandatory,Abréviation est obligatoire, -About the Company,À propos de l'entreprise, +About the Company,À propos de l'entreprise, About your company,A propos de votre entreprise, Above,Au-dessus, Absent,Absent, @@ -100,7 +100,7 @@ Active,actif, Active Leads / Customers,Prospects / Clients Actifs, Activity Cost exists for Employee {0} against Activity Type - {1},Des Coûts d'Activité existent pour l'Employé {0} pour le Type d'Activité - {1}, Activity Cost per Employee,Coût de l'Activité par Employé, -Activity Type,Type d'activité, +Activity Type,Type d'activité, Actual Cost,Prix actuel, Actual Delivery Date,Date de livraison réelle, Actual Qty,Quantité Réelle, @@ -129,7 +129,7 @@ Add Timesheets,Ajouter des feuilles de temps, Add Timeslots,Ajouter des Créneaux, Add Users to Marketplace,Ajouter des utilisateurs à la Marketplace, Add a new address,ajouter une nouvelle adresse, -Add cards or custom sections on homepage,Ajouter des cartes ou des sections personnalisées sur la page d'accueil, +Add cards or custom sections on homepage,Ajouter des cartes ou des sections personnalisées sur la page d'accueil, Add more items or open full form,Ajouter plus d'articles ou ouvrir le formulaire complet, Add notes,Ajouter des notes, Add the rest of your organization as your users. You can also add invite Customers to your portal by adding them from Contacts,Ajouter le reste de votre organisation en tant qu'utilisateurs. Vous pouvez aussi inviter des Clients sur votre portail en les ajoutant depuis les Contacts, @@ -196,7 +196,7 @@ All communications including and above this shall be moved into the new Issue,"T All items have already been invoiced,Tous les articles ont déjà été facturés, All items have already been transferred for this Work Order.,Tous les articles ont déjà été transférés pour cet ordre de travail., All other ITC,Tous les autres CTI, -All the mandatory Task for employee creation hasn't been done yet.,Toutes les tâches obligatoires pour la création d'employés n'ont pas encore été effectuées., +All the mandatory Task for employee creation hasn't been done yet.,Toutes les tâches obligatoires pour la création d'employés n'ont pas encore été effectuées., All these items have already been invoiced,Tous ces articles ont déjà été facturés, Allocate Payment Amount,Allouer le montant du paiement, Allocated Amount,Montant alloué, @@ -206,7 +206,7 @@ Allow Delete,Autoriser la suppression, Already record exists for the item {0},L'enregistrement existe déjà pour l'article {0}, "Already set default in pos profile {0} for user {1}, kindly disabled default","Déjà défini par défaut dans le profil pdv {0} pour l'utilisateur {1}, veuillez désactiver la valeur par défaut", Alternate Item,Article alternatif, -Alternative item must not be same as item code,L'article alternatif ne doit pas être le même que le code article, +Alternative item must not be same as item code,L'article alternatif ne doit pas être le même que le code article, Amended From,Modifié depuis, Amount,Montant, Amount After Depreciation,Montant après amortissement, @@ -221,14 +221,14 @@ Amount {0} {1} {2} {3},Montant {0} {1} {2} {3}, Amt,Nb, "An Item Group exists with same name, please change the item name or rename the item group","Un Groupe d'Article existe avec le même nom, veuillez changer le nom de l'article ou renommer le groupe d'article", An academic term with this 'Academic Year' {0} and 'Term Name' {1} already exists. Please modify these entries and try again.,Une période universitaire avec cette 'Année Universitaire' {0} et 'Nom de la Période' {1} existe déjà. Veuillez modifier ces entrées et essayer à nouveau., -An error occurred during the update process,Une erreur s'est produite lors du processus de mise à jour, +An error occurred during the update process,Une erreur s'est produite lors du processus de mise à jour, "An item exists with same name ({0}), please change the item group name or rename the item","Un article existe avec le même nom ({0}), veuillez changer le nom du groupe d'article ou renommer l'article", Analyst,Analyste, Analytics,Analytique, Annual Billing: {0},Facturation Annuelle : {0}, Annual Salary,Salaire annuel, Anonymous,Anonyme, -Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4},Un autre enregistrement Budget '{0}' existe déjà pour {1} '{2}' et pour le compte '{3}' pour l'exercice {4}., +Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4},Un autre enregistrement Budget '{0}' existe déjà pour {1} '{2}' et pour le compte '{3}' pour l'exercice {4}., Another Period Closing Entry {0} has been made after {1},Une autre Entrée de Clôture de Période {0} a été faite après {1}, Another Sales Person {0} exists with the same Employee id,Un autre Commercial {0} existe avec le même ID d'Employé, Antibiotic,Antibiotique, @@ -260,11 +260,11 @@ Approving User cannot be same as user the rule is Applicable To,L'Utilisateur Ap "Apps using current key won't be able to access, are you sure?","Les applications utilisant la clé actuelle ne pourront plus y accéder, êtes-vous sûr?", Are you sure you want to cancel this appointment?,Êtes-vous sûr de vouloir annuler ce rendez-vous?, Arrear,Arriéré, -As Examiner,En tant qu'examinateur, +As Examiner,En tant qu'examinateur, As On Date,Comme à la date, As Supervisor,En tant que superviseur, As per rules 42 & 43 of CGST Rules,Conformément aux règles 42 et 43 des règles de la CGST, -As per section 17(5),Conformément à l'article 17 (5), +As per section 17(5),Conformément à l'article 17 (5), As per your assigned Salary Structure you cannot apply for benefits,La struture salariale qui vous a été assignée ne vous permet pas de demander des avantages sociaux, Assessment,Évaluation, Assessment Criteria,Critères d'Évaluation, @@ -273,7 +273,7 @@ Assessment Group: ,Groupe d'Évaluation:, Assessment Plan,Plan d'Évaluation, Assessment Plan Name,Nom du Plan d'Évaluation, Assessment Report,Rapport d'Évaluation, -Assessment Reports,Rapports d'évaluation, +Assessment Reports,Rapports d'évaluation, Assessment Result,Résultat de l'Évaluation, Assessment Result record {0} already exists.,Le Résultat d'Évaluation {0} existe déjà., Asset,Atout, @@ -329,7 +329,7 @@ Available Selling,Vente disponible, Available for use date is required,La date de mise en service est nécessaire, Available slots,Créneaux Disponibles, Available {0},Disponible {0}, -Available-for-use Date should be after purchase date,La date de disponibilité devrait être postérieure à la date d'achat, +Available-for-use Date should be after purchase date,La date de disponibilité devrait être postérieure à la date d'achat, Average Age,Âge moyen, Average Rate,Prix moyen, Avg Daily Outgoing,Moy Quotidienne Sortante, @@ -384,7 +384,7 @@ Batch Name,Nom du lot, Batch No,N° du Lot, Batch number is mandatory for Item {0},Le numéro de lot est obligatoire pour l'Article {0}, Batch {0} of Item {1} has expired.,Lot {0} de l'Article {1} a expiré., -Batch {0} of Item {1} is disabled.,Le lot {0} de l'élément {1} est désactivé., +Batch {0} of Item {1} is disabled.,Le lot {0} de l'élément {1} est désactivé., Batch: ,Lot:, Batches,Lots, Become a Seller,Devenir vendeur, @@ -399,20 +399,20 @@ Billed,Facturé, Billed Amount,Montant facturé, Billing,Facturation, Billing Address,Adresse de facturation, -Billing Address is same as Shipping Address,L'adresse de facturation est identique à l'adresse de livraison, +Billing Address is same as Shipping Address,L'adresse de facturation est identique à l'adresse de livraison, Billing Amount,Montant de Facturation, Billing Status,Statut de la Facturation, Billing currency must be equal to either default company's currency or party account currency,La devise de facturation doit être égale à la devise de la société par défaut ou à la devise du compte du partenaire, Bills raised by Suppliers.,Factures émises par des Fournisseurs., Bills raised to Customers.,Factures émises pour des Clients., Biotechnology,Biotechnologie, -Birthday Reminder,Rappel d'anniversaire, +Birthday Reminder,Rappel d'anniversaire, Black,Noir, Blanket Orders from Costumers.,Commandes provisoires de clients., Block Invoice,Bloquer la facture, Boms,Listes de Matériaux, Bonus Payment Date cannot be a past date,La date de paiement du bonus ne peut pas être une date passée, -Both Trial Period Start Date and Trial Period End Date must be set,La date de début de la période d'essai et la date de fin de la période d'essai doivent être définies, +Both Trial Period Start Date and Trial Period End Date must be set,La date de début de la période d'essai et la date de fin de la période d'essai doivent être définies, Both Warehouse must belong to same Company,Les deux Entrepôt doivent appartenir à la même Société, Branch,Branche, Broadcasting,Radio/Télévision, @@ -429,7 +429,7 @@ Business Development Manager,Directeur Commercial, Buy,Acheter, Buying,Achat, Buying Amount,Montant d'Achat, -Buying Price List,Liste de prix d'achat, +Buying Price List,Liste de prix d'achat, Buying Rate,Prix d'achat, "Buying must be checked, if Applicable For is selected as {0}","Achat doit être vérifié, si Applicable Pour {0} est sélectionné", By {0},Par {0}, @@ -447,7 +447,7 @@ Campaign,Campagne, Can be approved by {0},Peut être approuvé par {0}, "Can not filter based on Account, if grouped by Account","Impossible de filtrer sur le Compte , si les lignes sont regroupées par Compte", "Can not filter based on Voucher No, if grouped by Voucher","Impossible de filtrer sur la base du N° de Coupon, si les lignes sont regroupées par Coupon", -"Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}","Impossible de marquer le dossier d'hospitalisation déchargé, il existe des factures non facturées {0}", +"Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}","Impossible de marquer le dossier d'hospitalisation déchargé, il existe des factures non facturées {0}", Can only make payment against unbilled {0},Le paiement n'est possible qu'avec les {0} non facturés, Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total',Peut se référer à ligne seulement si le type de charge est 'Montant de la ligne précedente' ou 'Total des lignes précedente', "Can't change valuation method, as there are transactions against some items which does not have it's own valuation method","Impossible de modifier la méthode de valorisation, car il existe des transactions sur certains articles ne possèdant pas leur propre méthode de valorisation", @@ -455,17 +455,17 @@ Can't create standard criteria. Please rename the criteria,Impossible de créer Cancel,Annuler, Cancel Material Visit {0} before cancelling this Warranty Claim,Annuler la Visite Matérielle {0} avant d'annuler cette Réclamation de Garantie, Cancel Material Visits {0} before cancelling this Maintenance Visit,Annuler les Visites Matérielles {0} avant d'annuler cette Visite de Maintenance, -Cancel Subscription,Annuler l'abonnement, +Cancel Subscription,Annuler l'abonnement, Cancel the journal entry {0} first,Annuler d'abord l'écriture de journal {0}, Canceled,Annulé, "Cannot Submit, Employees left to mark attendance","Ne peut pas être soumis, certains employés n'ont pas pas validé leurs feuilles de présence", Cannot be a fixed asset item as Stock Ledger is created.,Ne peut pas être un article immobilisé car un Journal de Stock a été créé., Cannot cancel because submitted Stock Entry {0} exists,Impossible d'annuler car l'Écriture de Stock soumise {0} existe, Cannot cancel transaction for Completed Work Order.,Impossible d'annuler la transaction lorsque l'ordre de travail est terminé., -Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3},Impossible d'annuler {0} {1} car le numéro de série {2} n'appartient pas à l'entrepôt {3}, +Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3},Impossible d'annuler {0} {1} car le numéro de série {2} n'appartient pas à l'entrepôt {3}, Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item,Impossible de modifier les attributs après des mouvements de stock. Faites un nouvel article et transférez la quantité en stock au nouvel article, Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved.,Impossible de modifier les dates de début et de fin d'exercice une fois que l'exercice est enregistré., -Cannot change Service Stop Date for item in row {0},Impossible de modifier la date d'arrêt du service pour l'élément de la ligne {0}, +Cannot change Service Stop Date for item in row {0},Impossible de modifier la date d'arrêt du service pour l'élément de la ligne {0}, Cannot change Variant properties after stock transaction. You will have to make a new Item to do this.,Impossible de modifier les propriétés de variante après une transaction de stock. Vous devrez créer un nouvel article pour pouvoir le faire., "Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency.","Impossible de changer la devise par défaut de la société, parce qu'il y a des opérations existantes. Les transactions doivent être annulées pour changer la devise par défaut.", Cannot change status as student {0} is linked with student application {1},Impossible de changer le statut car l'étudiant {0} est lié à la candidature de l'étudiant {1}, @@ -479,7 +479,7 @@ Cannot deduct when category is for 'Valuation' or 'Valuation and Total',Déducti Cannot deduct when category is for 'Valuation' or 'Vaulation and Total',Vous ne pouvez pas déduire lorsqu'une catégorie est pour 'Évaluation' ou 'Évaluation et Total', "Cannot delete Serial No {0}, as it is used in stock transactions","Impossible de supprimer les N° de série {0}, s'ils sont dans les mouvements de stock", Cannot enroll more than {0} students for this student group.,Inscription de plus de {0} étudiants impossible pour ce groupe d'étudiants., -Cannot find Item with this barcode,Impossible de trouver l'article avec ce code à barres, +Cannot find Item with this barcode,Impossible de trouver l'article avec ce code à barres, Cannot find active Leave Period,Impossible de trouver une période de congés active, Cannot produce more Item {0} than Sales Order quantity {1},Impossible de produire plus d'Article {0} que la quantité {1} du Bon de Commande, Cannot promote Employee with status Left,"Impossible de promouvoir un employé avec le statut ""Parti""", @@ -538,7 +538,7 @@ Cheques Required,Chèques requis, Cheques and Deposits incorrectly cleared,Chèques et Dépôts incorrectement compensés, Child Item should not be a Product Bundle. Please remove item `{0}` and save,Le sous-article ne doit pas être un ensemble de produit. S'il vous plaît retirer l'article `{0}` et sauver, Child Task exists for this Task. You can not delete this Task.,Une tâche enfant existe pour cette tâche. Vous ne pouvez pas supprimer cette tâche., -Child nodes can be only created under 'Group' type nodes,Les noeuds enfants peuvent être créés uniquement dans les nœuds de type 'Groupe', +Child nodes can be only created under 'Group' type nodes,Les noeuds enfants peuvent être créés uniquement dans les nœuds de type 'Groupe', Child warehouse exists for this warehouse. You can not delete this warehouse.,Un entrepôt enfant existe pour cet entrepôt. Vous ne pouvez pas supprimer cet entrepôt., Circular Reference Error,Erreur de référence circulaire, City,Ville, @@ -578,11 +578,11 @@ Commission rate cannot be greater than 100,Taux de commission ne peut pas être Community Forum,Forum de la communauté, Company (not Customer or Supplier) master.,Données de base de la Société (ni les Clients ni les Fournisseurs), Company Abbreviation,Abréviation de la Société, -Company Abbreviation cannot have more than 5 characters,L'abréviation de l'entreprise ne peut pas comporter plus de 5 caractères, +Company Abbreviation cannot have more than 5 characters,L'abréviation de l'entreprise ne peut pas comporter plus de 5 caractères, Company Name,Nom de la Société, Company Name cannot be Company,Nom de la Société ne peut pas être Company, Company currencies of both the companies should match for Inter Company Transactions.,Les devises des deux sociétés doivent correspondre pour les transactions inter-sociétés., -Company is manadatory for company account,La société est le maître d'œuvre du compte d'entreprise, +Company is manadatory for company account,La société est le maître d'œuvre du compte d'entreprise, Company name not same,Le nom de la société n'est pas identique, Company {0} does not exist,Société {0} n'existe pas, "Company, Payment Account, From Date and To Date is mandatory","Société, compte de paiement, date de début et date de fin sont obligatoires", @@ -653,7 +653,7 @@ Could not submit some Salary Slips,Les fiches de paie n'ont pas pu être soumise Country wise default Address Templates,Modèles d'Adresse par défaut en fonction du pays, Course,Cours, Course Code: ,Code du Cours:, -Course Enrollment {0} does not exists,L'inscription au cours {0} n'existe pas, +Course Enrollment {0} does not exists,L'inscription au cours {0} n'existe pas, Course Schedule,Horaire du cours, Course: ,Cours:, Cr,Cr, @@ -677,7 +677,7 @@ Create Leads,Créer des Prospects, Create Maintenance Visit,Créer une visite de maintenance, Create Material Request,Créer une demande de matériel, Create Multiple,Créer plusieurs, -Create Opening Sales and Purchase Invoices,Créer des factures d'ouverture et des factures d'achat, +Create Opening Sales and Purchase Invoices,Créer des factures d'ouverture et des factures d'achat, Create Payment Entries,Créer des entrées de paiement, Create Payment Entry,Créer une entrée de paiement, Create Print Format,Créer Format d'Impression, @@ -691,7 +691,7 @@ Create Sales Order,Créer une commande client, Create Sales Orders to help you plan your work and deliver on-time,Créez des commandes pour vous aider à planifier votre travail et à livrer à temps, Create Sample Retention Stock Entry,Créer un échantillon de stock de rétention, Create Student,Créer un étudiant, -Create Student Batch,Créer un lot d'étudiants, +Create Student Batch,Créer un lot d'étudiants, Create Student Groups,Créer des Groupes d'Étudiants, Create Supplier Quotation,Créer une offre fournisseur, Create Tax Template,Créer un modèle de taxe, @@ -706,11 +706,11 @@ Create customer quotes,Créer les devis client, Create rules to restrict transactions based on values.,Créer des règles pour restreindre les transactions basées sur les valeurs ., Created By,Établi par, Created {0} scorecards for {1} between: ,{0} fiches d'évaluations créées pour {1} entre:, -Creating Company and Importing Chart of Accounts,Création d'une société et importation d'un plan comptable, +Creating Company and Importing Chart of Accounts,Création d'une société et importation d'un plan comptable, Creating Fees,Création d'Honoraires, Creating Payment Entries......,Créer des écritures de paiement..., Creating Salary Slips...,Création des fiches de paie en cours..., -Creating student groups,Créer des groupes d'étudiants, +Creating student groups,Créer des groupes d'étudiants, Creating {0} Invoice,Création de {0} facture, Credit,Crédit, Credit ({0}),Crédit ({0}), @@ -763,7 +763,7 @@ Customer required for 'Customerwise Discount',Client requis pour appliquer une ' Customer {0} does not belong to project {1},Le Client {0} ne fait pas parti du projet {1}, Customer {0} is created.,Le client {0} est créé., Customers in Queue,Clients dans la File d'Attente, -Customize Homepage Sections,Personnaliser les sections de la page d'accueil, +Customize Homepage Sections,Personnaliser les sections de la page d'accueil, Customizing Forms,Personnalisation des formulaires, Daily Project Summary for {0},Récapitulatif quotidien du projet pour {0}, Daily Reminders,Rappels quotidiens, @@ -817,7 +817,7 @@ Del,Supp, Delay in payment (Days),Retard de paiement (jours), Delete all the Transactions for this Company,Supprimer toutes les transactions pour cette société, Delete permanently?,Supprimer définitivement ?, -Deletion is not permitted for country {0},La suppression n'est pas autorisée pour le pays {0}, +Deletion is not permitted for country {0},La suppression n'est pas autorisée pour le pays {0}, Delivered,Livré, Delivered Amount,Montant Livré, Delivered Qty,Qté Livrée, @@ -842,13 +842,13 @@ Depreciation Eliminated due to disposal of assets,Amortissement Eliminé en rais Depreciation Entry,Ecriture d’Amortissement, Depreciation Method,Méthode d'Amortissement, Depreciation Row {0}: Depreciation Start Date is entered as past date,Ligne de d'amortissement {0}: La date de début de l'amortissement est dans le passé, -Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1},Ligne d'amortissement {0}: la valeur attendue après la durée de vie utile doit être supérieure ou égale à {1}, +Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1},Ligne d'amortissement {0}: la valeur attendue après la durée de vie utile doit être supérieure ou égale à {1}, Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date,Ligne d'amortissement {0}: La date d'amortissement suivante ne peut pas être antérieure à la date de mise en service, -Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date,Ligne d'amortissement {0}: la date d'amortissement suivante ne peut pas être antérieure à la date d'achat, +Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date,Ligne d'amortissement {0}: la date d'amortissement suivante ne peut pas être antérieure à la date d'achat, Designer,Designer, Detailed Reason,Raison détaillée, Details,Détails, -Details of Outward Supplies and inward supplies liable to reverse charge,Détails des livraisons sortantes et des livraisons entrantes susceptibles d'inverser la charge, +Details of Outward Supplies and inward supplies liable to reverse charge,Détails des livraisons sortantes et des livraisons entrantes susceptibles d'inverser la charge, Details of the operations carried out.,Détails des opérations effectuées., Diagnosis,Diagnostique, Did not find any item called {0},N'a pas trouvé d'élément appelé {0}, @@ -872,7 +872,7 @@ Discount amount cannot be greater than 100%,Le montant de la réduction ne peut Discount must be less than 100,La remise doit être inférieure à 100, Diseases & Fertilizers,Maladies et engrais, Dispatch,Envoi, -Dispatch Notification,Notification d'expédition, +Dispatch Notification,Notification d'expédition, Dispatch State,Statut de l'expédition, Distance,Distance, Distribution,Distribution, @@ -900,7 +900,7 @@ Draft,Brouillon, Drop Ship,Expédition Directe, Drug,Médicament, Due / Reference Date cannot be after {0},Date d’échéance / de référence ne peut pas être après le {0}, -Due Date cannot be before Posting / Supplier Invoice Date,La date d'échéance ne peut pas être antérieure à la date de comptabilisation / facture fournisseur, +Due Date cannot be before Posting / Supplier Invoice Date,La date d'échéance ne peut pas être antérieure à la date de comptabilisation / facture fournisseur, Due Date is mandatory,La Date d’Échéance est obligatoire, Duplicate Entry. Please check Authorization Rule {0},Écriture en double. Merci de vérifier la Règle d’Autorisation {0}, Duplicate Serial No entered for Item {0},Dupliquer N° de Série pour l'Article {0}, @@ -920,7 +920,7 @@ Earnest Money,Arrhes, Earning,Revenus, Edit,modifier, Edit Publishing Details,Modifier les détails de publication, -"Edit in full page for more options like assets, serial nos, batches etc.","Modifier en pleine page pour plus d'options comme les actifs, les numéros de série, les lots, etc.", +"Edit in full page for more options like assets, serial nos, batches etc.","Modifier en pleine page pour plus d'options comme les actifs, les numéros de série, les lots, etc.", Education,Éducation, Either location or employee must be required,La localisation ou l'employé sont requis, Either target qty or target amount is mandatory,Soit la qté cible soit le montant cible est obligatoire, @@ -935,16 +935,16 @@ Email Address,Adresse électronique, Email Digest: ,Compte Rendu par Email :, Email Reminders will be sent to all parties with email contacts,Les rappels par emails seront envoyés à toutes les parties avec des contacts ayant une adresse email, Email Sent,Email envoyé, -Email Template,Modèle d'email, +Email Template,Modèle d'email, Email not found in default contact,Email non trouvé dans le contact par défaut, Email sent to supplier {0},Email envoyé au fournisseur {0}, Email sent to {0},Email envoyé à {0}, Employee,Employé, -Employee A/C Number,Numéro de l'employé, +Employee A/C Number,Numéro de l'employé, Employee Advances,Avances versées aux employés, Employee Benefits,Avantages de l'Employé, Employee Grade,Echelon des employés, -Employee ID,Numéro d'employé, +Employee ID,Numéro d'employé, Employee Lifecycle,Cycle de vie des employés, Employee Name,Nom de l'Employé, Employee Promotion cannot be submitted before Promotion Date ,La promotion ne peut être soumise avant la date de promotion, @@ -952,9 +952,9 @@ Employee Referral,Recommandations, Employee Transfer cannot be submitted before Transfer Date ,Le transfert ne peut pas être soumis avant la date de transfert, Employee cannot report to himself.,L'employé ne peut pas rendre de compte à lui-même., Employee relieved on {0} must be set as 'Left',Employé dégagé de {0} doit être défini comme 'Gauche', -Employee status cannot be set to 'Left' as following employees are currently reporting to this employee: ,Le statut d'employé ne peut pas être défini sur 'Gauche' car les employés suivants sont actuellement rattachés à cet employé:, +Employee status cannot be set to 'Left' as following employees are currently reporting to this employee: ,Le statut d'employé ne peut pas être défini sur 'Gauche' car les employés suivants sont actuellement rattachés à cet employé:, Employee {0} already submited an apllication {1} for the payroll period {2},L'employé {0} a déjà envoyé une demande {1} pour la période de calcul de paie {2}, -Employee {0} has already applied for {1} between {2} and {3} : ,L'employé {0} a déjà postulé pour {1} entre {2} et {3}:, +Employee {0} has already applied for {1} between {2} and {3} : ,L'employé {0} a déjà postulé pour {1} entre {2} et {3}:, Employee {0} has already applied for {1} on {2} : ,L'employé {0} a déjà postulé pour {1} le {2}:, Employee {0} has no maximum benefit amount,L'employé {0} n'a pas de montant maximal d'avantages sociaux, Employee {0} is not active or does not exist,"L'employé {0} n'est pas actif, ou n'existe pas", @@ -971,7 +971,7 @@ End Date cannot be before Start Date.,La date de fin ne peut pas être antérieu End Year,Année de Fin, End Year cannot be before Start Year,L'Année de Fin ne peut pas être avant l'Année de Début, End on,Termine le, -End time cannot be before start time,L'heure de fin ne peut pas être avant l'heure de début, +End time cannot be before start time,L'heure de fin ne peut pas être avant l'heure de début, Ends On date cannot be before Next Contact Date.,La date de fin ne peut pas être avant la prochaine date de contact, Energy,Énergie, Engineer,Ingénieur, @@ -1021,7 +1021,7 @@ Expense Claims,Notes de Frais, Expense account is mandatory for item {0},Compte de charge est obligatoire pour l'article {0}, Expense or Difference account is mandatory for Item {0} as it impacts overall stock value,Compte de Charge et d'Écarts est obligatoire pour objet {0} car il impacte la valeur globale des actions, Expenses,Charges, -Expenses Included In Asset Valuation,Dépenses incluses dans l'évaluation de l'actif, +Expenses Included In Asset Valuation,Dépenses incluses dans l'évaluation de l'actif, Expenses Included In Valuation,Charges Incluses dans la Valorisation, Expired Batches,Lots expirés, Expires On,Expire le, @@ -1034,7 +1034,7 @@ Extra Small,Très Petit, Fail,Échec, Failed,Échoué, Failed to create website,Échec de la création du site Web, -Failed to install presets,Échec de l'installation des préréglages, +Failed to install presets,Échec de l'installation des préréglages, Failed to login,Échec de la connexion, Failed to setup company,Échec de la configuration de la société, Failed to setup defaults,Échec de la configuration par défaut, @@ -1066,16 +1066,16 @@ Financial Statements,États financiers, Financial Year,Exercice Financier, Finish,terminer, Finished Good,Produit fini, -Finished Good Item Code,Code d'article fini, +Finished Good Item Code,Code d'article fini, Finished Goods,Produits finis, Finished Item {0} must be entered for Manufacture type entry,Le Produit Fini {0} doit être saisi pour une écriture de type Production, Finished product quantity {0} and For Quantity {1} cannot be different,La quantité de produit fini {0} et Pour la quantité {1} ne peut pas être différente, First Name,Prénom, -"Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}","Le régime fiscal est obligatoire, veuillez définir le régime fiscal de l'entreprise {0}", +"Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}","Le régime fiscal est obligatoire, veuillez définir le régime fiscal de l'entreprise {0}", Fiscal Year,Exercice fiscal, -Fiscal Year End Date should be one year after Fiscal Year Start Date,La date de fin d'exercice doit être un an après la date de début d'exercice, +Fiscal Year End Date should be one year after Fiscal Year Start Date,La date de fin d'exercice doit être un an après la date de début d'exercice, Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0},La Date de Début et la Date de Fin de l'Exercice Fiscal sont déjà définies dans l'Année Fiscale {0}, -Fiscal Year Start Date should be one year earlier than Fiscal Year End Date,La date de début d'exercice doit être un an plus tôt que la date de fin d'exercice, +Fiscal Year Start Date should be one year earlier than Fiscal Year End Date,La date de début d'exercice doit être un an plus tôt que la date de fin d'exercice, Fiscal Year {0} does not exist,Exercice Fiscal {0} n'existe pas, Fiscal Year {0} is required,Exercice Fiscal {0} est nécessaire, Fiscal Year {0} not found,Exercice Fiscal {0} introuvable, @@ -1086,8 +1086,8 @@ Fixed Assets,Actifs Immobilisés, Following Material Requests have been raised automatically based on Item's re-order level,Les Demandes de Matériel suivantes ont été créées automatiquement sur la base du niveau de réapprovisionnement de l’Article, Following accounts might be selected in GST Settings:,Les comptes suivants peuvent être sélectionnés dans les paramètres GST:, Following course schedules were created,Les horaires de cours suivants ont été créés, -Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master,L'élément suivant {0} n'est pas marqué comme élément {1}. Vous pouvez les activer en tant qu'élément {1} à partir de sa fiche article., -Following items {0} are not marked as {1} item. You can enable them as {1} item from its Item master,Les éléments suivants {0} ne sont pas marqués comme {1} élément. Vous pouvez les activer en tant qu'élément {1} à partir de sa fiche article., +Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master,L'élément suivant {0} n'est pas marqué comme élément {1}. Vous pouvez les activer en tant qu'élément {1} à partir de sa fiche article., +Following items {0} are not marked as {1} item. You can enable them as {1} item from its Item master,Les éléments suivants {0} ne sont pas marqués comme {1} élément. Vous pouvez les activer en tant qu'élément {1} à partir de sa fiche article., Food,Alimentation, "Food, Beverage & Tobacco","Alimentation, boissons et tabac", For,Pour, @@ -1099,14 +1099,14 @@ For Warehouse,Pour l’Entrepôt, For Warehouse is required before Submit,Pour l’Entrepôt est requis avant de Soumettre, "For an item {0}, quantity must be negative number","Pour l'article {0}, la quantité doit être un nombre négatif", "For an item {0}, quantity must be positive number","Pour un article {0}, la quantité doit être un nombre positif", -"For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry","Pour la carte de travail {0}, vous pouvez uniquement saisir une entrée de stock de type "Transfert d'article pour fabrication".", +"For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry","Pour la carte de travail {0}, vous pouvez uniquement saisir une entrée de stock de type "Transfert d'article pour fabrication".", "For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included","Pour la ligne {0} dans {1}. Pour inclure {2} dans le prix de l'Article, les lignes {3} doivent également être incluses", For row {0}: Enter Planned Qty,Pour la ligne {0}: entrez la quantité planifiée, "For {0}, only credit accounts can be linked against another debit entry","Pour {0}, seuls les comptes de crédit peuvent être liés avec une autre écriture de débit", "For {0}, only debit accounts can be linked against another credit entry","Pour {0}, seuls les comptes de débit peuvent être liés avec une autre écriture de crédit", Form View,Vue de Formulaire, Forum Activity,Activité du forum, -Free item code is not selected,Le code d'article gratuit n'est pas sélectionné, +Free item code is not selected,Le code d'article gratuit n'est pas sélectionné, Freight and Forwarding Charges,Frais de Fret et d'Expédition, Frequency,Fréquence, Friday,Vendredi, @@ -1122,7 +1122,7 @@ From Date {0} cannot be after employee's relieving Date {1},La date de début {0 From Date {0} cannot be before employee's joining Date {1},La date de départ {0} ne peut pas être antérieure à la date d'arrivée de l'employé {1}, From Datetime,A partir du (Date et Heure), From Delivery Note,Du Bon de Livraison, -From Fiscal Year,À partir de l'année fiscale, +From Fiscal Year,À partir de l'année fiscale, From GSTIN,GSTIN (Origine), From Party Name,Nom du tiers (Origine), From Pin Code,Code postal (Origine), @@ -1132,7 +1132,7 @@ From State,Etat (Origine), From Time,Horaire de Début, From Time Should Be Less Than To Time,Du temps devrait être moins que du temps, From Time cannot be greater than To Time.,L’Horaire Initial ne peut pas être postérieur à l’Horaire Final, -"From a supplier under composition scheme, Exempt and Nil rated","De la part d'un fournisseur sous schéma de composition, coté Exempt et Nil", +"From a supplier under composition scheme, Exempt and Nil rated","De la part d'un fournisseur sous schéma de composition, coté Exempt et Nil", From and To dates required,Les date Du et Au sont requises, From date can not be less than employee's joining date,La date de départ ne peut être antérieure à la date d'arrivée de l'employé, From value must be less than to value in row {0},De la valeur doit être inférieure à la valeur de la ligne {0}, @@ -1184,7 +1184,7 @@ Goals cannot be empty,Les objectifs ne peuvent pas être vides, Goods In Transit,Les marchandises en transit, Goods Transferred,Marchandises transférées, Goods and Services Tax (GST India),Taxe sur les Biens et Services (GST India), -Goods are already received against the outward entry {0},Les marchandises sont déjà reçues pour l'entrée sortante {0}, +Goods are already received against the outward entry {0},Les marchandises sont déjà reçues pour l'entrée sortante {0}, Government,Gouvernement, Grand Total,Total TTC, Grant,Subvention, @@ -1229,11 +1229,11 @@ Health Care,Soins de santé, Healthcare,Santé, Healthcare (beta),Santé (beta), Healthcare Practitioner,Praticien de la santé, -Healthcare Practitioner not available on {0},Le praticien de la santé n'est pas disponible le {0}, -Healthcare Practitioner {0} not available on {1},Le praticien de la santé {0} n'est pas disponible le {1}, +Healthcare Practitioner not available on {0},Le praticien de la santé n'est pas disponible le {0}, +Healthcare Practitioner {0} not available on {1},Le praticien de la santé {0} n'est pas disponible le {1}, Healthcare Service Unit,Service de soins de santé, Healthcare Service Unit Tree,Arbre des services de soins de santé, -Healthcare Service Unit Type,Type d'unité de service de soins de santé, +Healthcare Service Unit Type,Type d'unité de service de soins de santé, Healthcare Services,Services de santé, Healthcare Settings,Paramètres de santé, Hello,Bonjour, @@ -1244,7 +1244,7 @@ Hold,Tenir, Hold Invoice,Facture en attente, Holiday,Vacances, Holiday List,Liste de vacances, -Hotel Rooms of type {0} are unavailable on {1},Les chambres d'hôtel de type {0} sont indisponibles le {1}, +Hotel Rooms of type {0} are unavailable on {1},Les chambres d'hôtel de type {0} sont indisponibles le {1}, Hotels,Hôtels, Hourly,Horaire, Hours,Heures, @@ -1272,7 +1272,7 @@ Ignore Existing Ordered Qty,Ignorer la quantité commandée existante, Image,Image, Image View,Voir l'Image, Import Data,Importer des données, -Import Day Book Data,Données du journal d'importation, +Import Day Book Data,Données du journal d'importation, Import Log,Journal d'import, Import Master Data,Importer des données de base, Import Successfull,Importation réussie, @@ -1308,14 +1308,14 @@ Indirect Income,Revenu indirect, Individual,Individuel, Ineligible ITC,CTI non éligible, Initiated,Initié, -Inpatient Record,Dossier d'hospitalisation, +Inpatient Record,Dossier d'hospitalisation, Insert,Insérer, Installation Note,Note d'Installation, Installation Note {0} has already been submitted,Note d'Installation {0} à déjà été sousmise, Installation date cannot be before delivery date for Item {0},Date d'installation ne peut pas être avant la date de livraison pour l'Article {0}, Installing presets,Installation des réglages, Institute Abbreviation,Abréviation de l'Institut, -Institute Name,Nom de l'Institut, +Institute Name,Nom de l'Institut, Instructor,Instructeur, Insufficient Stock,Stock insuffisant, Insurance Start date should be less than Insurance End date,Date de Début d'Assurance devrait être antérieure à la Date de Fin d'Assurance, @@ -1331,8 +1331,8 @@ Invalid Attribute,Attribut invalide, Invalid Blanket Order for the selected Customer and Item,Commande avec limites non valide pour le client et l'article sélectionnés, Invalid Company for Inter Company Transaction.,Société non valide pour une transaction inter-sociétés., Invalid GSTIN! A GSTIN must have 15 characters.,GSTIN invalide! Un GSTIN doit comporter 15 caractères., -Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.,GSTIN invalide! Les deux premiers chiffres de GSTIN doivent correspondre au numéro d'état {0}., -Invalid GSTIN! The input you've entered doesn't match the format of GSTIN.,GSTIN invalide! L'entrée que vous avez entrée ne correspond pas au format de GSTIN., +Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.,GSTIN invalide! Les deux premiers chiffres de GSTIN doivent correspondre au numéro d'état {0}., +Invalid GSTIN! The input you've entered doesn't match the format of GSTIN.,GSTIN invalide! L'entrée que vous avez entrée ne correspond pas au format de GSTIN., Invalid Posting Time,Heure de publication non valide, Invalid attribute {0} {1},Attribut invalide {0} {1}, Invalid quantity specified for item {0}. Quantity should be greater than 0.,Quantité spécifiée invalide pour l'élément {0}. Quantité doit être supérieur à 0., @@ -1351,12 +1351,12 @@ Invoice Posting Date,Date d’Envois de la Facture, Invoice Type,Type de facture, Invoice already created for all billing hours,Facture déjà créée pour toutes les heures facturées, Invoice can't be made for zero billing hour,La facture ne peut pas être faite pour une heure facturée à zéro, -Invoice {0} no longer exists,La facture {0} n'existe plus, +Invoice {0} no longer exists,La facture {0} n'existe plus, Invoiced,Facturé, Invoiced Amount,Montant facturé, Invoices,Factures, Invoices for Costumers.,Factures pour les clients., -Inward Supplies(liable to reverse charge,Approvisionnement entrant (susceptible d'inverser la charge, +Inward Supplies(liable to reverse charge,Approvisionnement entrant (susceptible d'inverser la charge, Inward supplies from ISD,Approvisionnement entrant de la DSI, Inward supplies liable to reverse charge (other than 1 & 2 above),Approvisionnements entrants susceptibles d’être dédouanés (autres que 1 et 2 ci-dessus), Is Active,Est Active, @@ -1383,17 +1383,17 @@ Item Description,Description de l'Article, Item Group,Groupe d'Article, Item Group Tree,Arborescence de Groupe d'Article, Item Group not mentioned in item master for item {0},Le Groupe d'Articles n'est pas mentionné dans la fiche de l'article pour l'article {0}, -Item Name,Nom de l'article, +Item Name,Nom de l'article, Item Price added for {0} in Price List {1},Prix de l'Article ajouté pour {0} dans la Liste de Prix {1}, -"Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates.","Le prix de l'article apparaît plusieurs fois en fonction de la liste de prix, du fournisseur / client, de la devise, de l'article, de l'unité de mesure, de la quantité et des dates.", +"Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates.","Le prix de l'article apparaît plusieurs fois en fonction de la liste de prix, du fournisseur / client, de la devise, de l'article, de l'unité de mesure, de la quantité et des dates.", Item Price updated for {0} in Price List {1},Prix de l'Article mis à jour pour {0} dans la Liste des Prix {1}, -Item Row {0}: {1} {2} does not exist in above '{1}' table,Ligne d'objet {0}: {1} {2} n'existe pas dans la table '{1}' ci-dessus, +Item Row {0}: {1} {2} does not exist in above '{1}' table,Ligne d'objet {0}: {1} {2} n'existe pas dans la table '{1}' ci-dessus, Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable,La Ligne de Taxe d'Article {0} doit indiquer un compte de type Taxes ou Produit ou Charge ou Facturable, -Item Template,Modèle d'article, +Item Template,Modèle d'article, Item Variant Settings,Paramètres de Variante d'Article, Item Variant {0} already exists with same attributes,La Variante de l'Article {0} existe déjà avec les mêmes caractéristiques, Item Variants,Variantes de l'Article, -Item Variants updated,Variantes d'article mises à jour, +Item Variants updated,Variantes d'article mises à jour, Item has variants.,L'article a des variantes., Item must be added using 'Get Items from Purchase Receipts' button,L'article doit être ajouté à l'aide du bouton 'Obtenir des éléments de Reçus d'Achat', Item or Warehouse for row {0} does not match Material Request,L'Article ou l'Entrepôt pour la ligne {0} ne correspond pas avec la Requête de Matériel, @@ -1422,12 +1422,12 @@ Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1},Artic Item {0}: Ordered qty {1} cannot be less than minimum order qty {2} (defined in Item).,L'article {0} : Qté commandée {1} ne peut pas être inférieure à la qté de commande minimum {2} (défini dans l'Article)., Item: {0} does not exist in the system,Article : {0} n'existe pas dans le système, Items,Articles, -Items Filter,Filtre d'articles, +Items Filter,Filtre d'articles, Items and Pricing,Articles et prix, Items for Raw Material Request,Articles pour demande de matière première, Job Card,Carte de travail, Job Description,Description de l'Emploi, -Job Offer,Offre d'emploi, +Job Offer,Offre d'emploi, Job card {0} created,Job card {0} créée, Jobs,Emplois, Join,Joindre, @@ -1455,7 +1455,7 @@ Last Communication Date,Date de la Dernière Communication, Last Name,Nom de famille, Last Order Amount,Montant de la Dernière Commande, Last Order Date,Date de la dernière commande, -Last Purchase Price,Dernier prix d'achat, +Last Purchase Price,Dernier prix d'achat, Last Purchase Rate,Dernier Prix d'Achat, Latest,Dernier, Latest price updated in all BOMs,Prix les plus récents mis à jour dans toutes les LDMs, @@ -1479,7 +1479,7 @@ Leave Type {0} cannot be carry-forwarded,Le Type de Congé {0} ne peut pas être Leave Type {0} is not encashable,Le type de congé {0} n'est pas encaissable, Leave Without Pay,Congé Sans Solde, Leave and Attendance,Congés et Présences, -Leave application {0} already exists against the student {1},Laisser l'application {0} existe déjà pour l'étudiant {1}, +Leave application {0} already exists against the student {1},Laisser l'application {0} existe déjà pour l'étudiant {1}, "Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}","Congé ne peut être alloué avant le {0}, car le solde de congés a déjà été reporté dans la feuille d'allocation de congés futurs {1}", "Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}","Congé ne peut être demandé / annulé avant le {0}, car le solde de congés a déjà été reporté dans la feuille d'allocation de congés futurs {1}", Leave of type {0} cannot be longer than {1},Les Congés de type {0} ne peuvent pas être plus long que {1}, @@ -1543,7 +1543,7 @@ Maintenance Visit {0} must be cancelled before cancelling this Sales Order,La Vi Maintenance start date can not be before delivery date for Serial No {0},La date de début d'entretien ne peut pas être antérieure à la date de livraison pour le N° de Série {0}, Make,Faire, Make Payment,Faire un Paiement, -Make project from a template.,Faire un projet à partir d'un modèle., +Make project from a template.,Faire un projet à partir d'un modèle., Making Stock Entries,Faire des Écritures de Stock, Male,Masculin, Manage Customer Group Tree.,Gérer l'Arborescence des Groupes de Clients., @@ -1593,16 +1593,16 @@ Material Request {0} submitted.,Demande de matériel {0} soumise., Material Transfer,Transfert de matériel, Material Transferred,Matériel transféré, Material to Supplier,Du Matériel au Fournisseur, -Max Exemption Amount cannot be greater than maximum exemption amount {0} of Tax Exemption Category {1},Le montant maximal de l'exemption ne peut pas dépasser le montant maximal de l'exonération {0} de la catégorie d'exonération fiscale {1}., +Max Exemption Amount cannot be greater than maximum exemption amount {0} of Tax Exemption Category {1},Le montant maximal de l'exemption ne peut pas dépasser le montant maximal de l'exonération {0} de la catégorie d'exonération fiscale {1}., Max benefits should be greater than zero to dispense benefits,Les prestations sociales maximales doivent être supérieures à zéro pour être calculées, Max discount allowed for item: {0} is {1}%,Réduction max autorisée pour l'article : {0} est de {1} %, Max: {0},Max : {0}, -Maximum Samples - {0} can be retained for Batch {1} and Item {2}.,Maximum d'échantillons - {0} peut être conservé pour le lot {1} et l'article {2}., -Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}.,Nombre maximum d'échantillons - {0} ont déjà été conservés pour le lot {1} et l'article {2} dans le lot {3}., +Maximum Samples - {0} can be retained for Batch {1} and Item {2}.,Maximum d'échantillons - {0} peut être conservé pour le lot {1} et l'article {2}., +Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}.,Nombre maximum d'échantillons - {0} ont déjà été conservés pour le lot {1} et l'article {2} dans le lot {3}., Maximum amount eligible for the component {0} exceeds {1},Le montant maximal éligible pour le composant {0} dépasse {1}, Maximum benefit amount of component {0} exceeds {1},La quantité maximale de prestations sociales du composant {0} dépasse {1}, Maximum benefit amount of employee {0} exceeds {1},Le montant maximal des prestations sociales de l'employé {0} dépasse {1}, -Maximum discount for Item {0} is {1}%,La remise maximale pour l'article {0} est {1}%, +Maximum discount for Item {0} is {1}%,La remise maximale pour l'article {0} est {1}%, Maximum leave allowed in the leave type {0} is {1},La durée maximale autorisée pour le type de congé {0} est {1}, Medical,Médical, Medical Code,Code médical, @@ -1616,9 +1616,9 @@ Member ID,ID du membre, Member Name,Nom de membre, Member information.,Informations sur le membre, Membership,Adhésion, -Membership Details,Détails de l'adhésion, -Membership ID,ID d'adhésion, -Membership Type,Type d'adhésion, +Membership Details,Détails de l'adhésion, +Membership ID,ID d'adhésion, +Membership Type,Type d'adhésion, Memebership Details,Détails de l'adhésion, Memebership Type Details,Détails du type d'adhésion, Merge,Fusionner, @@ -1636,8 +1636,8 @@ Min Qty can not be greater than Max Qty,Qté Min ne peut pas être supérieure Minimum Lead Age (Days),Âge Minimum du Prospect (Jours), Miscellaneous Expenses,Charges Diverses, Missing Currency Exchange Rates for {0},Taux de Change Manquant pour {0}, -Missing email template for dispatch. Please set one in Delivery Settings.,Modèle de courrier électronique manquant pour l'envoi. Veuillez en définir un dans les paramètres de livraison., -"Missing value for Password, API Key or Shopify URL","Valeur manquante pour le mot de passe, la clé API ou l'URL Shopify", +Missing email template for dispatch. Please set one in Delivery Settings.,Modèle de courrier électronique manquant pour l'envoi. Veuillez en définir un dans les paramètres de livraison., +"Missing value for Password, API Key or Shopify URL","Valeur manquante pour le mot de passe, la clé API ou l'URL Shopify", Mode of Payment,Moyen de paiement, Mode of Payments,Mode de paiement, Mode of Transport,Mode de transport, @@ -1651,7 +1651,7 @@ Monthly Distribution,Répartition Mensuelle, Monthly Repayment Amount cannot be greater than Loan Amount,Montant du Remboursement Mensuel ne peut pas être supérieur au Montant du Prêt, More,Plus, More Information,Informations Complémentaires, -More than one selection for {0} not allowed,Plus d'une sélection pour {0} non autorisée, +More than one selection for {0} not allowed,Plus d'une sélection pour {0} non autorisée, More...,Plus..., Motion Picture & Video,Cinéma & Vidéo, Move,mouvement, @@ -1719,7 +1719,7 @@ Next,Suivant, Next Contact By cannot be same as the Lead Email Address,Prochain Contact Par ne peut être identique à l’Adresse Email du Prospect, Next Contact Date cannot be in the past,La Date de Prochain Contact ne peut pas être dans le passé, Next Steps,Prochaines étapes, -No Action,Pas d'action, +No Action,Pas d'action, No Customers yet!,Pas encore de clients!, No Data,Aucune Donnée, No Delivery Note selected for Customer {},Aucun bon de livraison sélectionné pour le client {}, @@ -1737,11 +1737,11 @@ No Permission,Aucune autorisation, No Quote,Aucun Devis, No Remarks,Aucune Remarque, No Result to submit,Aucun résultat à soumettre, -No Salary Structure assigned for Employee {0} on given date {1},Aucune structure de salaire attribuée à l'employé {0} à la date donnée {1}, +No Salary Structure assigned for Employee {0} on given date {1},Aucune structure de salaire attribuée à l'employé {0} à la date donnée {1}, No Staffing Plans found for this Designation,Aucun plan de dotation trouvé pour cette désignation, No Student Groups created.,Aucun Groupe d'Étudiants créé., No Students in,Aucun étudiant dans, -No Tax Withholding data found for the current Fiscal Year.,Aucune donnée de retenue d'impôt trouvée pour l'exercice en cours., +No Tax Withholding data found for the current Fiscal Year.,Aucune donnée de retenue d'impôt trouvée pour l'exercice en cours., No Work Orders created,Aucun ordre de travail créé, No accounting entries for the following warehouses,Pas d’écritures comptables pour les entrepôts suivants, No active or default Salary Structure found for employee {0} for the given dates,Aucune Structure de Salaire active ou par défaut trouvée pour employé {0} pour les dates données, @@ -1753,12 +1753,12 @@ No description given,Aucune Description, No employees for the mentioned criteria,Aucun employé pour les critères mentionnés, No gain or loss in the exchange rate,Aucun gain ou perte dans le taux de change, No items listed,Aucun article référencé, -No items to be received are overdue,Aucun article à recevoir n'est en retard, +No items to be received are overdue,Aucun article à recevoir n'est en retard, No material request created,Aucune demande de matériel créée, No more updates,Pas de mise à jour supplémentaire, -No of Interactions,Nombre d'interactions, -No of Shares,Nombre d'actions, -No pending Material Requests found to link for the given items.,Aucune demande de matériel en attente n'a été trouvée pour créer un lien vers les articles donnés., +No of Interactions,Nombre d'interactions, +No of Shares,Nombre d'actions, +No pending Material Requests found to link for the given items.,Aucune demande de matériel en attente n'a été trouvée pour créer un lien vers les articles donnés., No products found,Aucun produit trouvé, No products found.,Aucun produit trouvé., No record found,Aucun Enregistrement Trouvé, @@ -1769,7 +1769,7 @@ No salary slip found to submit for the above selected criteria OR salary slip al No tasks,Aucune tâche, No time sheets,Aucunes feuilles de temps, No values,Pas de valeurs, -No {0} found for Inter Company Transactions.,Aucun {0} n'a été trouvé pour les transactions inter-sociétés., +No {0} found for Inter Company Transactions.,Aucun {0} n'a été trouvé pour les transactions inter-sociétés., Non GST Inward Supplies,Fournitures entrantes non liées à la TPS, Non Profit,À But Non Lucratif, Non Profit (beta),Association (bêta), @@ -1792,7 +1792,7 @@ Not eligible for the admission in this program as per DOB,Non admissible à l'ad Not items found,Pas d'objets trouvés, Not permitted for {0},Non autorisé pour {0}, "Not permitted, configure Lab Test Template as required","Non autorisé, veuillez configurer le modèle de test de laboratoire", -Not permitted. Please disable the Service Unit Type,Pas permis. Veuillez désactiver le type d'unité de service, +Not permitted. Please disable the Service Unit Type,Pas permis. Veuillez désactiver le type d'unité de service, Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s),Remarque : Date de Référence / d’Échéance dépasse le nombre de jours de crédit client autorisé de {0} jour(s), Note: Item {0} entered multiple times,Remarque : Article {0} saisi plusieurs fois, Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified,Remarque : Écriture de Paiement ne sera pas créée car le compte 'Compte Bancaire ou de Caisse' n'a pas été spécifié, @@ -1801,7 +1801,7 @@ Note: There is not enough leave balance for Leave Type {0},Remarque : Le solde d Note: This Cost Center is a Group. Cannot make accounting entries against groups.,Remarque : Ce Centre de Coûts est un Groupe. Vous ne pouvez pas faire des écritures comptables sur des groupes., Note: {0},Note : {0}, Notes,Remarques, -Nothing is included in gross,Rien n'est inclus dans le brut, +Nothing is included in gross,Rien n'est inclus dans le brut, Nothing more to show.,Rien de plus à montrer., Nothing to change,Rien à changer, Notice Period,Période de préavis, @@ -1824,8 +1824,8 @@ Online,En ligne, Online Auctions,Enchères en ligne, Only Leave Applications with status 'Approved' and 'Rejected' can be submitted,Seules les Demandes de Congés avec le statut 'Appouvée' ou 'Rejetée' peuvent être soumises, "Only the Student Applicant with the status ""Approved"" will be selected in the table below.",Seul les candidatures étudiantes avec le statut «Approuvé» seront sélectionnées dans le tableau ci-dessous., -Only users with {0} role can register on Marketplace,Seuls les utilisateurs ayant le rôle {0} peuvent s'inscrire sur Marketplace, -Only {0} in stock for item {1},Seulement {0} en stock pour l'article {1}, +Only users with {0} role can register on Marketplace,Seuls les utilisateurs ayant le rôle {0} peuvent s'inscrire sur Marketplace, +Only {0} in stock for item {1},Seulement {0} en stock pour l'article {1}, Open BOM {0},Ouvrir LDM {0}, Open Item {0},Ouvrir l'Article {0}, Open Notifications,Notifications ouvertes, @@ -1837,13 +1837,13 @@ Opening (Dr),Ouverture (Dr), Opening Accounting Balance,Solde d'Ouverture de Comptabilité, Opening Accumulated Depreciation,Amortissement Cumulé d'Ouverture, Opening Accumulated Depreciation must be less than equal to {0},Amortissement Cumulé d'Ouverture doit être inférieur ou égal à {0}, -Opening Balance,Solde d'ouverture, +Opening Balance,Solde d'ouverture, Opening Balance Equity,Ouverture de la Balance des Capitaux Propres, Opening Date and Closing Date should be within same Fiscal Year,Date d'Ouverture et Date de Clôture devraient être dans le même Exercice, Opening Date should be before Closing Date,Date d'Ouverture devrait être antérieure à la Date de Clôture, Opening Entry Journal,Ecriture de journal d'ouverture, -Opening Invoice Creation Tool,Ouverture de l'outil de création de facture, -Opening Invoice Item,Ouverture d'un poste de facture, +Opening Invoice Creation Tool,Ouverture de l'outil de création de facture, +Opening Invoice Item,Ouverture d'un poste de facture, Opening Invoices,Ouverture des factures, Opening Invoices Summary,Ouverture des factures Résumé, Opening Qty,Quantité d'Ouverture, @@ -1861,7 +1861,7 @@ Opp/Lead %,Opp / Prospect %, Opportunities,Opportunités, Opportunities by lead source,Opportunités par source de plomb, Opportunity,Opportunité, -Opportunity Amount,Montant de l'opportunité, +Opportunity Amount,Montant de l'opportunité, Optional Holiday List not set for leave period {0},Une liste de vacances facultative n'est pas définie pour la période de congé {0}, "Optional. Sets company's default currency, if not specified.","Optionnel. Défini la devise par défaut de l'entreprise, si non spécifié.", Optional. This setting will be used to filter in various transactions.,Facultatif. Ce paramètre sera utilisé pour filtrer différentes transactions., @@ -1925,7 +1925,7 @@ Party Type and Party is mandatory for {0} account,Le type de tiers et le tiers s Party Type is mandatory,Type de Tiers Obligatoire, Party is mandatory,Le Tiers est obligatoire, Password,Mot de passe, -Password policy for Salary Slips is not set,La politique de mot de passe pour les bulletins de salaire n'est pas définie, +Password policy for Salary Slips is not set,La politique de mot de passe pour les bulletins de salaire n'est pas définie, Past Due Date,Date d'échéance dépassée, Patient,Patient, Patient Appointment,Rendez-vous patient, @@ -1971,7 +1971,7 @@ Payments,Paiements, Payroll,Paie, Payroll Number,Numéro de paie, Payroll Payable,Paie à Payer, -Payroll date can not be less than employee's joining date,La date de paie ne peut être inférieure à la date d'adhésion de l'employé, +Payroll date can not be less than employee's joining date,La date de paie ne peut être inférieure à la date d'adhésion de l'employé, Payslip,Fiche de paie, Pending Activities,Activités en attente, Pending Amount,Montant en attente, @@ -1994,17 +1994,17 @@ Physician,Médecin, Piecework,Travail à la pièce, Pin Code,Code PIN, Pincode,Code Postal, -Place Of Supply (State/UT),Lieu d'approvisionnement (State / UT), +Place Of Supply (State/UT),Lieu d'approvisionnement (State / UT), Place Order,Passer la commande, Plan Name,Nom du plan, Plan for maintenance visits.,Plan pour les visites de maintenance., Planned Qty,Qté Planifiée, -"Planned Qty: Quantity, for which, Work Order has been raised, but is pending to be manufactured.",Qté prévue: quantité pour laquelle l'ordre de travail a été créé mais sa fabrication est en attente., +"Planned Qty: Quantity, for which, Work Order has been raised, but is pending to be manufactured.",Qté prévue: quantité pour laquelle l'ordre de travail a été créé mais sa fabrication est en attente., Planning,Planification, Plants and Machineries,Usines et Machines, Please Set Supplier Group in Buying Settings.,Veuillez définir un groupe de fournisseurs par défaut dans les paramètres d'achat., -Please add a Temporary Opening account in Chart of Accounts,Veuillez ajouter un compte d'ouverture temporaire dans le plan comptable, -Please add the account to root level Company - ,S'il vous plaît ajouter le compte au niveau racine Société -, +Please add a Temporary Opening account in Chart of Accounts,Veuillez ajouter un compte d'ouverture temporaire dans le plan comptable, +Please add the account to root level Company - ,S'il vous plaît ajouter le compte au niveau racine Société -, Please add the remaining benefits {0} to any of the existing component,Veuillez ajouter les prestations restantes {0} à l'un des composants existants, Please check Multi Currency option to allow accounts with other currency,Veuillez vérifier l'option Multi-Devises pour permettre les comptes avec une autre devise, Please click on 'Generate Schedule',"Veuillez cliquer sur ""Générer calendrier''", @@ -2013,7 +2013,7 @@ Please click on 'Generate Schedule' to get schedule,Veuillez cliquer sur ‘Gén Please confirm once you have completed your training,Veuillez confirmer une fois que vous avez terminé votre formation, Please contact to the user who have Sales Master Manager {0} role,Veuillez contactez l'utilisateur qui a le rôle de Directeur des Ventes {0}, Please create Customer from Lead {0},Veuillez créer un client à partir du prospect {0}, -Please create purchase receipt or purchase invoice for the item {0},Veuillez créer un reçu d'achat ou une facture d'achat pour l'article {0}, +Please create purchase receipt or purchase invoice for the item {0},Veuillez créer un reçu d'achat ou une facture d'achat pour l'article {0}, Please define grade for Threshold 0%,Veuillez définir une note pour le Seuil 0%, Please enable Applicable on Booking Actual Expenses,Veuillez activer l'option : Applicable sur la base de l'enregistrement des dépenses réelles, Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses,Veuillez activer les options : Applicable sur la base des bons de commande d'achat et Applicable sur la base des bons de commande d'achat, @@ -2056,11 +2056,11 @@ Please enter repayment Amount,Veuillez entrer le Montant de remboursement, Please enter valid Financial Year Start and End Dates,Veuillez entrer des Dates de Début et de Fin d’Exercice Comptable valides, Please enter valid email address,Entrez une adresse email valide, Please enter {0} first,Veuillez d’abord entrer {0}, -Please fill in all the details to generate Assessment Result.,Veuillez renseigner tous les détails pour générer le résultat de l'évaluation., +Please fill in all the details to generate Assessment Result.,Veuillez renseigner tous les détails pour générer le résultat de l'évaluation., Please identify/create Account (Group) for type - {0},Veuillez identifier / créer un compte (groupe) pour le type - {0}, Please identify/create Account (Ledger) for type - {0},Veuillez identifier / créer un compte (grand livre) pour le type - {0}, Please input all required Result Value(s),Veuillez entrer toutes les valeurs de résultat requises, -Please login as another user to register on Marketplace,Veuillez vous connecter en tant qu'autre utilisateur pour vous inscrire sur Marketplace, +Please login as another user to register on Marketplace,Veuillez vous connecter en tant qu'autre utilisateur pour vous inscrire sur Marketplace, Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.,Veuillez vous assurer que vous voulez vraiment supprimer tous les transactions de cette société. Vos données de base resteront intactes. Cette action ne peut être annulée., Please mention Basic and HRA component in Company,Veuillez mentionner les composants Basic et HRA dans la société, Please mention Round Off Account in Company,Veuillez indiquer le Compte d’Arrondi de la Société, @@ -2085,10 +2085,10 @@ Please select Company and Designation,Veuillez sélectionner la société et la Please select Company and Party Type first,Veuillez d’abord sélectionner une Société et le Type de Tiers, Please select Company and Posting Date to getting entries,Veuillez sélectionner la société et la date de comptabilisation pour obtenir les écritures, Please select Company first,Veuillez d’abord sélectionner une Société, -Please select Completion Date for Completed Asset Maintenance Log,Veuillez sélectionner la date d'achèvement pour le journal de maintenance des actifs terminé, -Please select Completion Date for Completed Repair,Veuillez sélectionner la date d'achèvement pour la réparation terminée, +Please select Completion Date for Completed Asset Maintenance Log,Veuillez sélectionner la date d'achèvement pour le journal de maintenance des actifs terminé, +Please select Completion Date for Completed Repair,Veuillez sélectionner la date d'achèvement pour la réparation terminée, Please select Course,Veuillez sélectionner un cours, -Please select Drug,S'il vous plaît sélectionnez Drug, +Please select Drug,S'il vous plaît sélectionnez Drug, Please select Employee,Veuillez sélectionner un employé, Please select Employee Record first.,Veuillez d’abord sélectionner le Dossier de l'Employé., Please select Existing Company for creating Chart of Accounts,Veuillez sélectionner une Société Existante pour créer un Plan de Compte, @@ -2102,7 +2102,7 @@ Please select Posting Date before selecting Party,Veuillez sélectionner la Date Please select Posting Date first,Veuillez d’abord sélectionner la Date de Comptabilisation, Please select Price List,Veuillez sélectionner une Liste de Prix, Please select Program,Veuillez sélectionner un programme, -Please select Qty against item {0},Veuillez sélectionner Qté par rapport à l'élément {0}, +Please select Qty against item {0},Veuillez sélectionner Qté par rapport à l'élément {0}, Please select Sample Retention Warehouse in Stock Settings first,Veuillez d'abord définir un entrepôt de stockage des échantillons dans les paramètres de stock, Please select Start Date and End Date for Item {0},Veuillez sélectionner la Date de Début et Date de Fin pour l'Article {0}, Please select Student Admission which is mandatory for the paid student applicant,Veuillez sélectionner obligatoirement une Admission d'Étudiant pour la candidature étudiante payée, @@ -2111,7 +2111,7 @@ Please select a Batch for Item {0}. Unable to find a single batch that fulfills Please select a Company,Veuillez sélectionner une Société, Please select a batch,Veuillez sélectionner un lot, Please select a csv file,Veuillez sélectionner un fichier csv, -Please select a customer,S'il vous plaît sélectionner un client, +Please select a customer,S'il vous plaît sélectionner un client, Please select a field to edit from numpad,Veuillez sélectionner un champ à modifier sur le pavé numérique, Please select a table,Veuillez sélectionner une table, Please select a valid Date,Veuillez sélectionner une date valide, @@ -2127,7 +2127,7 @@ Please select month and year,Veuillez sélectionner le mois et l'année, Please select prefix first,Veuillez d’abord sélectionner un préfixe, Please select the Company,Veuillez sélectionner la société, Please select the Company first,Veuillez sélectionner la Société en premier, -Please select the Multiple Tier Program type for more than one collection rules.,Veuillez sélectionner le type de programme à plusieurs niveaux pour plus d'une règle de collecte., +Please select the Multiple Tier Program type for more than one collection rules.,Veuillez sélectionner le type de programme à plusieurs niveaux pour plus d'une règle de collecte., Please select the assessment group other than 'All Assessment Groups',Sélectionnez un groupe d'évaluation autre que «Tous les Groupes d'Évaluation», Please select the document type first,Veuillez d’abord sélectionner le type de document, Please select weekly off day,Veuillez sélectionnez les jours de congé hebdomadaires, @@ -2149,9 +2149,9 @@ Please set Number of Depreciations Booked,Veuillez définir le Nombre d’Amorti Please set Unrealized Exchange Gain/Loss Account in Company {0},Veuillez définir un compte de gain / perte de change non réalisé pour la société {0}, Please set User ID field in an Employee record to set Employee Role,Veuillez définir le champ ID de l'Utilisateur dans un dossier Employé pour définir le Rôle de l’Employés, Please set a default Holiday List for Employee {0} or Company {1},Veuillez définir une Liste de Vacances par défaut pour l'Employé {0} ou la Société {1}, -Please set account in Warehouse {0},Veuillez définir un compte dans l'entrepôt {0}, +Please set account in Warehouse {0},Veuillez définir un compte dans l'entrepôt {0}, Please set an active menu for Restaurant {0},Veuillez définir un menu actif pour le restaurant {0}, -Please set associated account in Tax Withholding Category {0} against Company {1},Veuillez définir le compte associé dans la catégorie de retenue d'impôt {0} contre la société {1}., +Please set associated account in Tax Withholding Category {0} against Company {1},Veuillez définir le compte associé dans la catégorie de retenue d'impôt {0} contre la société {1}., Please set at least one row in the Taxes and Charges Table,Veuillez définir au moins une ligne dans le tableau des taxes et des frais., Please set default Cash or Bank account in Mode of Payment {0},Veuillez définir un compte de Caisse ou de Banque par défaut pour le Mode de Paiement {0}, Please set default account in Salary Component {0},Veuillez définir le compte par défaut dans la Composante Salariale {0}, @@ -2161,17 +2161,17 @@ Please set default template for Leave Approval Notification in HR Settings.,Veui Please set default template for Leave Status Notification in HR Settings.,Veuillez définir un modèle par défaut pour la notification de statut de congés dans les paramètres RH., Please set default {0} in Company {1},Veuillez définir {0} par défaut dans la Société {1}, Please set filter based on Item or Warehouse,Veuillez définir un filtre basé sur l'Article ou l'Entrepôt, -Please set leave policy for employee {0} in Employee / Grade record,Veuillez définir la politique de congé pour l'employé {0} dans le dossier Employé / Grade, +Please set leave policy for employee {0} in Employee / Grade record,Veuillez définir la politique de congé pour l'employé {0} dans le dossier Employé / Grade, Please set recurring after saving,Veuillez définir la récurrence après avoir sauvegardé, Please set the Company,Veuillez définir la Société, -Please set the Customer Address,Veuillez définir l'adresse du client, +Please set the Customer Address,Veuillez définir l'adresse du client, Please set the Date Of Joining for employee {0},Veuillez définir la Date d'Embauche pour l'employé {0}, Please set the Default Cost Center in {0} company.,Veuillez définir un centre de coûts par défaut pour la société {0}., Please set the Email ID for the Student to send the Payment Request,Configurez l'ID de courrier électronique pour que l'Élève envoie la Demande de Paiement, Please set the Item Code first,Veuillez définir le Code d'Article en premier, Please set the Payment Schedule,Veuillez définir le calendrier de paiement, Please set the series to be used.,Veuillez définir la série à utiliser., -Please set {0} for address {1},Définissez {0} pour l'adresse {1}., +Please set {0} for address {1},Définissez {0} pour l'adresse {1}., Please setup Students under Student Groups,Veuillez configurer les Étudiants sous des groupes d'Étudiants, Please share your feedback to the training by clicking on 'Training Feedback' and then 'New',"Partagez vos commentaires sur la formation en cliquant sur 'Retour d'Expérience de la formation', puis 'Nouveau'", Please specify Company,Veuillez spécifier la Société, @@ -2227,13 +2227,13 @@ Pricing Rule,Règle de tarification, Pricing Rule {0} is updated,La règle de tarification {0} est mise à jour, Pricing Rules are further filtered based on quantity.,Les Règles de Tarification sont d'avantage filtrés en fonction de la quantité., Primary,Primaire, -Primary Address Details,Détails de l'adresse principale, +Primary Address Details,Détails de l'adresse principale, Primary Contact Details,Détails du contact principal, Principal Amount,Montant Principal, -Print Format,Format d'impression, +Print Format,Format d'impression, Print IRS 1099 Forms,Imprimer les formulaires IRS 1099, Print Report Card,Imprimer le rapport, -Print Settings,Paramètres d'impression, +Print Settings,Paramètres d'impression, Print and Stationery,Impression et Papeterie, Print settings updated in respective print format,Paramètres d'impression mis à jour avec le format d'impression indiqué, Print taxes with zero amount,Impression de taxes avec un montant nul, @@ -2261,7 +2261,7 @@ Profit and Loss,Pertes et Profits, Profit for the year,Bénéfice de l'exercice, Program,Programme, Program in the Fee Structure and Student Group {0} are different.,Le programme dans la structure d'honoraires et le groupe d'étudiants {0} sont différents., -Program {0} does not exist.,Le programme {0} n'existe pas., +Program {0} does not exist.,Le programme {0} n'existe pas., Program: ,Programme:, Progress % for a task cannot be more than 100.,% de Progression pour une tâche ne peut pas être supérieur à 100., Project Collaboration Invitation,Invitation de Collaboration à un Projet, @@ -2301,7 +2301,7 @@ Purchase Order,Bon de commande, Purchase Order Amount,Bon de commande, Purchase Order Amount(Company Currency),Montant du bon de commande (devise de la société), Purchase Order Date,Date du bon de commande, -Purchase Order Items not received on time,Articles de commande d'achat non reçus à temps, +Purchase Order Items not received on time,Articles de commande d'achat non reçus à temps, Purchase Order number required for Item {0},Numéro de Bon de Commande requis pour l'Article {0}, Purchase Order to Payment,Du Bon de Commande au Paiement, Purchase Order {0} is not submitted,Le Bon de Commande {0} n’est pas soumis, @@ -2324,7 +2324,7 @@ Quality,Qualité, Quality Action,Action Qualité, Quality Goal.,Objectif de qualité., Quality Inspection,Inspection de la Qualité, -Quality Inspection: {0} is not submitted for the item: {1} in row {2},Contrôle qualité: {0} n'est pas soumis pour l'élément: {1} à la ligne {2}., +Quality Inspection: {0} is not submitted for the item: {1} in row {2},Contrôle qualité: {0} n'est pas soumis pour l'élément: {1} à la ligne {2}., Quality Management,Gestion de la qualité, Quality Meeting,Réunion de qualité, Quality Procedure,Procédure de qualité, @@ -2371,12 +2371,12 @@ Reading Uploaded File,Lecture du fichier téléchargé, Real Estate,Immobilier, Reason For Putting On Hold,Raison de la mise en attente, Reason for Hold,Raison de tenir, -Reason for hold: ,Raison de l'attente:, +Reason for hold: ,Raison de l'attente:, Receipt,Reçu, Receipt document must be submitted,Le reçu doit être soumis, Receivable,Créance, Receivable Account,Compte Débiteur, -Receive at Warehouse Entry,Recevoir à l'entrée de l'entrepôt, +Receive at Warehouse Entry,Recevoir à l'entrée de l'entrepôt, Received,Reçu, Received On,Reçu le, Received Quantity,Quantité reçue, @@ -2402,7 +2402,7 @@ Reference No.,Numéro de référence, Reference Number,Numéro de réference, Reference Owner,Responsable de la Référence, Reference Type,Type de référence, -"Reference: {0}, Item Code: {1} and Customer: {2}","Référence: {0}, Code de l'article: {1} et Client: {2}", +"Reference: {0}, Item Code: {1} and Customer: {2}","Référence: {0}, Code de l'article: {1} et Client: {2}", References,Références, Refresh Token,Jeton de Rafraîchissement, Region,Région, @@ -2413,7 +2413,7 @@ Related,en relation, Relation with Guardian1,Relation avec Tuteur1, Relation with Guardian2,Relation avec Tuteur2, Release Date,Date de la fin de mise en attente, -Reload Linked Analysis,Recharger l'analyse liée, +Reload Linked Analysis,Recharger l'analyse liée, Remaining,Restant, Remaining Balance,Solde restant, Remarks,Remarques, @@ -2460,16 +2460,16 @@ Reserved Qty,Qté Réservées, Reserved Qty for Production,Qté Réservée pour la Production, Reserved Qty for Production: Raw materials quantity to make manufacturing items.,Qté réservée à la production: quantité de matières premières permettant de fabriquer des articles de fabrication., "Reserved Qty: Quantity ordered for sale, but not delivered.","Réservés Quantité: Quantité de commande pour la vente , mais pas livré .", -Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied,L'entrepôt réservé est obligatoire pour l'article {0} dans les matières premières fournies, +Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied,L'entrepôt réservé est obligatoire pour l'article {0} dans les matières premières fournies, Reserved for manufacturing,Réservé pour la production, Reserved for sale,Réservé à la vente, Reserved for sub contracting,Réservé à la sous-traitance, Resistant,Résistant, -Resolve error and upload again.,Résoudre l'erreur et télécharger à nouveau., +Resolve error and upload again.,Résoudre l'erreur et télécharger à nouveau., Response,Réponse, Responsibilities,Responsabilités, Rest Of The World,Reste du monde, -Restart Subscription,Redémarrer l'abonnement, +Restart Subscription,Redémarrer l'abonnement, Restaurant,Restaurant, Result Date,Date de résultat, Result already Submitted,Résultat déjà soumis, @@ -2479,13 +2479,13 @@ Retail & Wholesale,Vente de Détail & en Gros, Retail Operations,Opérations de détail, Retained Earnings,Bénéfices Non Répartis, Retention Stock Entry,Entrée de stock de rétention, -Retention Stock Entry already created or Sample Quantity not provided,Saisie de stock de rétention déjà créée ou quantité d'échantillon non fournie, +Retention Stock Entry already created or Sample Quantity not provided,Saisie de stock de rétention déjà créée ou quantité d'échantillon non fournie, Return,Retour, Return / Credit Note,Retour / Note de crédit, Return / Debit Note,Retour / Note de Débit, Returns,Retours, Reverse Journal Entry,Ecriture de journal de contre-passation, -Review Invitation Sent,Examiner l'invitation envoyée, +Review Invitation Sent,Examiner l'invitation envoyée, Review and Action,Révision et action, Role,Rôle, Rooms Booked,Chambres réservées, @@ -2506,10 +2506,10 @@ Row # {0}: Serial No is mandatory,Ligne # {0} : N° de série est obligatoire, Row # {0}: Serial No {1} does not match with {2} {3},Ligne # {0} : N° de série {1} ne correspond pas à {2} {3}, Row #{0} (Payment Table): Amount must be negative,Row # {0} (Table de paiement): le montant doit être négatif, Row #{0} (Payment Table): Amount must be positive,Ligne #{0} (Table de paiement): Le montant doit être positif, -Row #{0}: Account {1} does not belong to company {2},Ligne # {0}: le compte {1} n'appartient pas à la société {2}, +Row #{0}: Account {1} does not belong to company {2},Ligne # {0}: le compte {1} n'appartient pas à la société {2}, Row #{0}: Allocated Amount cannot be greater than outstanding amount.,Ligne # {0}: montant attribué ne peut pas être supérieur au montant en souffrance., "Row #{0}: Asset {1} cannot be submitted, it is already {2}","Ligne #{0} : L’Actif {1} ne peut pas être soumis, il est déjà {2}", -Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.,Ligne n ° {0}: impossible de définir le tarif si le montant est supérieur au montant facturé pour l'élément {1}., +Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.,Ligne n ° {0}: impossible de définir le tarif si le montant est supérieur au montant facturé pour l'élément {1}., Row #{0}: Clearance date {1} cannot be before Cheque Date {2},Ligne #{0} : Date de compensation {1} ne peut pas être antérieure à la Date du Chèque {2}, Row #{0}: Duplicate entry in References {1} {2},Ligne # {0}: entrée en double dans les références {1} {2}, Row #{0}: Expected Delivery Date cannot be before Purchase Order Date,Ligne {0}: la date de livraison prévue ne peut pas être avant la date de commande, @@ -2527,15 +2527,15 @@ Row #{0}: Rejected Qty can not be entered in Purchase Return,Ligne #{0} : Qté R Row #{0}: Rejected Warehouse is mandatory against rejected Item {1},Ligne #{0} : Entrepôt de Rejet est obligatoire pour l’Article rejeté {1}, Row #{0}: Reqd by Date cannot be before Transaction Date,La ligne # {0}: Reqd par date ne peut pas être antérieure à la date de la transaction, Row #{0}: Set Supplier for item {1},Ligne #{0} : Définir Fournisseur pour l’article {1}, -Row #{0}: Status must be {1} for Invoice Discounting {2},Ligne n ° {0}: l'état doit être {1} pour l'actualisation de facture {2}., +Row #{0}: Status must be {1} for Invoice Discounting {2},Ligne n ° {0}: l'état doit être {1} pour l'actualisation de facture {2}., "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","Ligne # {0}: Le lot {1} n'a que {2} qté(s). Veuillez sélectionner un autre lot contenant {3} qtés disponible ou diviser la rangée en plusieurs lignes, pour livrer / émettre à partir de plusieurs lots", Row #{0}: Timings conflicts with row {1},Ligne #{0}: Minutage en conflit avec la ligne {1}, Row #{0}: {1} can not be negative for item {2},Ligne #{0} : {1} ne peut pas être négatif pour l’article {2}, Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2},Ligne N° {0}: Le montant ne peut être supérieur au Montant en Attente pour la Note de Frais {1}. Le Montant en Attente est de {2}, -Row {0} : Operation is required against the raw material item {1},Ligne {0}: l'opération est requise pour l'article de matière première {1}, +Row {0} : Operation is required against the raw material item {1},Ligne {0}: l'opération est requise pour l'article de matière première {1}, Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2},La ligne {0} # Montant alloué {1} ne peut pas être supérieure au montant non réclamé {2}, -Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3},La ligne {0} # article {1} ne peut pas être transférée plus de {2} par commande d'achat {3}, -Row {0}# Paid Amount cannot be greater than requested advance amount,La ligne {0} # Montant payé ne peut pas être supérieure au montant de l'avance demandée, +Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3},La ligne {0} # article {1} ne peut pas être transférée plus de {2} par commande d'achat {3}, +Row {0}# Paid Amount cannot be greater than requested advance amount,La ligne {0} # Montant payé ne peut pas être supérieure au montant de l'avance demandée, Row {0}: Activity Type is mandatory.,Ligne {0} : Le Type d'Activité est obligatoire., Row {0}: Advance against Customer must be credit,Ligne {0} : L’Avance du Client doit être un crédit, Row {0}: Advance against Supplier must be debit,Ligne {0} : L’Avance du Fournisseur doit être un débit, @@ -2548,7 +2548,7 @@ Row {0}: Cost center is required for an item {1},Ligne {0}: le Centre de Coûts Row {0}: Credit entry can not be linked with a {1},Ligne {0} : L’Écriture de crédit ne peut pas être liée à un {1}, Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2},Ligne {0} : La devise de la LDM #{1} doit être égale à la devise sélectionnée {2}, Row {0}: Debit entry can not be linked with a {1},Ligne {0} : L’Écriture de Débit ne peut pas être lié à un {1}, -Row {0}: Depreciation Start Date is required,Ligne {0}: la date de début de l'amortissement est obligatoire, +Row {0}: Depreciation Start Date is required,Ligne {0}: la date de début de l'amortissement est obligatoire, Row {0}: Enter location for the asset item {1},Ligne {0}: entrez la localisation de l'actif {1}, Row {0}: Exchange Rate is mandatory,Ligne {0} : Le Taux de Change est obligatoire, Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount,Ligne {0}: la valeur attendue après la durée de vie utile doit être inférieure au montant brut de l'achat, @@ -2562,19 +2562,19 @@ Row {0}: Party / Account does not match with {1} / {2} in {3} {4},Ligne {0} : Ti Row {0}: Party Type and Party is required for Receivable / Payable account {1},Ligne {0} : Le Type de Tiers et le Tiers sont requis pour le compte Débiteur / Créditeur {1}, Row {0}: Payment against Sales/Purchase Order should always be marked as advance,Ligne {0} : Paiements contre Commandes Client / Fournisseur doivent toujours être marqués comme des avances, Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.,Ligne {0} : Veuillez vérifier 'Est Avance' sur le compte {1} si c'est une avance., -Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges,Ligne {0}: Définissez le motif d'exemption de taxe dans les taxes de vente et les frais., +Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges,Ligne {0}: Définissez le motif d'exemption de taxe dans les taxes de vente et les frais., Row {0}: Please set the Mode of Payment in Payment Schedule,Ligne {0}: Veuillez définir le mode de paiement dans le calendrier de paiement., Row {0}: Please set the correct code on Mode of Payment {1},Ligne {0}: définissez le code correct sur le mode de paiement {1}., Row {0}: Qty is mandatory,Ligne {0} : Qté obligatoire, -Row {0}: Quality Inspection rejected for item {1},Ligne {0}: le contrôle qualité a été rejeté pour l'élément {1}., +Row {0}: Quality Inspection rejected for item {1},Ligne {0}: le contrôle qualité a été rejeté pour l'élément {1}., Row {0}: UOM Conversion Factor is mandatory,Ligne {0} : Facteur de Conversion LDM est obligatoire, -Row {0}: select the workstation against the operation {1},Ligne {0}: sélectionnez le poste de travail en fonction de l'opération {1}, +Row {0}: select the workstation against the operation {1},Ligne {0}: sélectionnez le poste de travail en fonction de l'opération {1}, Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.,Ligne {0}: {1} Numéros de série requis pour l'article {2}. Vous en avez fourni {3}., -Row {0}: {1} is required to create the Opening {2} Invoices,La ligne {0}: {1} est requise pour créer les factures d'ouverture {2}, +Row {0}: {1} is required to create the Opening {2} Invoices,La ligne {0}: {1} est requise pour créer les factures d'ouverture {2}, Row {0}: {1} must be greater than 0,Ligne {0}: {1} doit être supérieure à 0, Row {0}: {1} {2} does not match with {3},Ligne {0} : {1} {2} ne correspond pas à {3}, Row {0}:Start Date must be before End Date,Ligne {0} : La Date de Début doit être avant la Date de Fin, -Rows with duplicate due dates in other rows were found: {0},Des lignes avec des dates d'échéance en double dans les autres lignes ont été trouvées: {0}, +Rows with duplicate due dates in other rows were found: {0},Des lignes avec des dates d'échéance en double dans les autres lignes ont été trouvées: {0}, Rules for adding shipping costs.,Règles pour l'ajout de frais de port., Rules for applying pricing and discount.,Règles pour l’application des tarifs et des remises., S.O. No.,S.O. N°., @@ -2586,10 +2586,10 @@ Salary Slip ID,ID Fiche de Paie, Salary Slip of employee {0} already created for this period,Fiche de Paie de l'employé {0} déjà créée pour cette période, Salary Slip of employee {0} already created for time sheet {1},Fiche de Paie de l'employé {0} déjà créée pour la feuille de temps {1}, Salary Slip submitted for period from {0} to {1},Fiche de paie soumise pour la période du {0} au {1}, -Salary Structure Assignment for Employee already exists,La structure de la structure salariale pour l'employé existe déjà, +Salary Structure Assignment for Employee already exists,La structure de la structure salariale pour l'employé existe déjà, Salary Structure Missing,Grille des Salaires Manquante, -Salary Structure must be submitted before submission of Tax Ememption Declaration,La structure salariale doit être soumise avant la soumission de la déclaration d'émigration fiscale, -Salary Structure not found for employee {0} and date {1},Structure de salaire non trouvée pour l'employé {0} et la date {1}, +Salary Structure must be submitted before submission of Tax Ememption Declaration,La structure salariale doit être soumise avant la soumission de la déclaration d'émigration fiscale, +Salary Structure not found for employee {0} and date {1},Structure de salaire non trouvée pour l'employé {0} et la date {1}, Salary Structure should have flexible benefit component(s) to dispense benefit amount,La structure salariale devrait comporter une ou plusieurs composantes de prestation sociales variables pour la distribution du montant de la prestation, "Salary already processed for period between {0} and {1}, Leave application period cannot be between this date range.","Salaire déjà traité pour la période entre {0} et {1}, La période de demande de congé ne peut pas être entre cette plage de dates.", Sales,Ventes, @@ -2626,7 +2626,7 @@ Same item cannot be entered multiple times.,Le même article ne peut pas être e Same supplier has been entered multiple times,Le même fournisseur a été saisi plusieurs fois, Sample,Échantillon, Sample Collection,Collecte d'Échantillons, -Sample quantity {0} cannot be more than received quantity {1},La quantité d'échantillon {0} ne peut pas dépasser la quantité reçue {1}, +Sample quantity {0} cannot be more than received quantity {1},La quantité d'échantillon {0} ne peut pas dépasser la quantité reçue {1}, Sanctioned,Sanctionné, Sanctioned Amount,Montant Approuvé, Sanctioned Amount cannot be greater than Claim Amount in Row {0}.,Le Montant Approuvé ne peut pas être supérieur au Montant Réclamé à la ligne {0}., @@ -2636,12 +2636,12 @@ Saved,Enregistré, Saving {0},Enregistrement {0}, Scan Barcode,Scan Code Barre, Schedule,Calendrier, -Schedule Admission,Calendrier d'admission, +Schedule Admission,Calendrier d'admission, Schedule Course,Cours Calendrier, Schedule Date,Date du Calendrier, Schedule Discharge,Décharge horaire, Scheduled,Prévu, -Scheduled Upto,Programmé jusqu'à, +Scheduled Upto,Programmé jusqu'à, "Schedules for {0} overlaps, do you want to proceed after skiping overlaped slots ?","Les plannings pour {0} se chevauchent, voulez-vous continuer sans prendre en compte les créneaux qui se chevauchent ?", Score cannot be greater than Maximum Score,Score ne peut pas être supérieure à Score maximum, Score must be less than or equal to 5,Score doit être inférieur ou égal à 5, @@ -2666,7 +2666,7 @@ See past orders,Voir les commandes passées, See past quotations,Voir les citations passées, Select,Sélectionner, Select Alternate Item,Sélectionnez un autre élément, -Select Attribute Values,Sélectionner les valeurs d'attribut, +Select Attribute Values,Sélectionner les valeurs d'attribut, Select BOM,Sélectionner LDM, Select BOM and Qty for Production,Sélectionner la LDM et la Qté pour la Production, "Select BOM, Qty and For Warehouse","Sélectionner une nomenclature, une quantité et un entrepôt", @@ -2703,7 +2703,7 @@ Select or add new customer,Sélectionner ou ajoutez nouveau client, Select students manually for the Activity based Group,Sélectionner les élèves manuellement pour un Groupe basé sur l'Activité, Select the customer or supplier.,Veuillez sélectionner le client ou le fournisseur., Select the nature of your business.,Sélectionner la nature de votre entreprise., -Select the program first,Sélectionnez d'abord le programme, +Select the program first,Sélectionnez d'abord le programme, Select to add Serial Number.,Sélectionnez pour ajouter un numéro de série., Select your Domains,Sélectionnez vos domaines, Selected Price List should have buying and selling fields checked.,La liste de prix sélectionnée doit avoir les champs d'achat et de vente cochés., @@ -2724,7 +2724,7 @@ Sent,Envoyé, Serial #,Série #, Serial No and Batch,N° de Série et lot, Serial No is mandatory for Item {0},N° de Série est obligatoire pour l'Article {0}, -Serial No {0} does not belong to Batch {1},Le numéro de série {0} n'appartient pas au lot {1}, +Serial No {0} does not belong to Batch {1},Le numéro de série {0} n'appartient pas au lot {1}, Serial No {0} does not belong to Delivery Note {1},N° de Série {0} ne fait pas partie du Bon de Livraison {1}, Serial No {0} does not belong to Item {1},N° de Série {0} n'appartient pas à l'Article {1}, Serial No {0} does not belong to Warehouse {1},N° de Série {0} ne fait pas partie de l’Entrepôt {1}, @@ -2753,11 +2753,11 @@ Service Expense,Frais de service, Service Level Agreement,Contrat de niveau de service, Service Level Agreement.,Contrat de niveau de service., Service Level.,Niveau de service., -Service Stop Date cannot be after Service End Date,La date d'arrêt du service ne peut pas être postérieure à la date de fin du service, -Service Stop Date cannot be before Service Start Date,La date d'arrêt du service ne peut pas être antérieure à la date de début du service, +Service Stop Date cannot be after Service End Date,La date d'arrêt du service ne peut pas être postérieure à la date de fin du service, +Service Stop Date cannot be before Service Start Date,La date d'arrêt du service ne peut pas être antérieure à la date de début du service, Services,Services, "Set Default Values like Company, Currency, Current Fiscal Year, etc.","Définir les Valeurs par Défaut comme : Societé, Devise, Exercice Actuel, etc...", -Set Details,Détails de l'ensemble, +Set Details,Détails de l'ensemble, Set New Release Date,Définir la nouvelle date de fin de mise en attente, Set Project and all Tasks to status {0}?,Définir le projet et toutes les tâches sur le statut {0}?, Set Status,Définir le statut, @@ -2769,15 +2769,15 @@ Set as Lost,Définir comme perdu, Set as Open,Définir comme ouvert, Set default inventory account for perpetual inventory,Configurer le compte d'inventaire par défaut pour l'inventaire perpétuel, Set default mode of payment,Définir le mode de paiement par défaut, -Set this if the customer is a Public Administration company.,Définissez cette option si le client est une société d'administration publique., -Set {0} in asset category {1} or company {2},Définissez {0} dans la catégorie d'actifs {1} ou la société {2}, +Set this if the customer is a Public Administration company.,Définissez cette option si le client est une société d'administration publique., +Set {0} in asset category {1} or company {2},Définissez {0} dans la catégorie d'actifs {1} ou la société {2}, "Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}","Définir les Événements à {0}, puisque l'employé attaché au Commercial ci-dessous n'a pas d'ID Utilisateur {1}", Setting defaults,Définition des valeurs par défaut, Setting up Email,Configurer l'Email, Setting up Email Account,Configuration du Compte Email, Setting up Employees,Configuration des Employés, Setting up Taxes,Configuration des Impôts, -Setting up company,Création d'entreprise, +Setting up company,Création d'entreprise, Settings,Paramètres, "Settings for online shopping cart such as shipping rules, price list etc.","Paramètres du panier tels que les règles de livraison, liste de prix, etc.", Settings for website homepage,Paramètres de la page d'accueil du site, @@ -2799,9 +2799,9 @@ Ship To State,Ship To State, Shipments,Livraisons, Shipping,livraison, Shipping Address,Adresse de livraison, -"Shipping Address does not have country, which is required for this Shipping Rule","L'adresse de livraison n'a pas de pays, ce qui est requis pour cette règle d'expédition", -Shipping rule only applicable for Buying,Règle d'expédition applicable uniquement pour l'achat, -Shipping rule only applicable for Selling,Règle d'expédition applicable uniquement pour la vente, +"Shipping Address does not have country, which is required for this Shipping Rule","L'adresse de livraison n'a pas de pays, ce qui est requis pour cette règle d'expédition", +Shipping rule only applicable for Buying,Règle d'expédition applicable uniquement pour l'achat, +Shipping rule only applicable for Selling,Règle d'expédition applicable uniquement pour la vente, Shopify Supplier,Fournisseur Shopify, Shopping Cart,Panier, Shopping Cart Settings,Paramètres du panier, @@ -2809,9 +2809,9 @@ Short Name,Nom court, Shortage Qty,Qté de Pénurie, Show Completed,Montrer terminé, Show Cumulative Amount,Afficher le montant cumulatif, -Show Employee,Afficher l'employé, +Show Employee,Afficher l'employé, Show Open,Afficher ouverte, -Show Opening Entries,Afficher les entrées d'ouverture, +Show Opening Entries,Afficher les entrées d'ouverture, Show Payment Details,Afficher les détails du paiement, Show Return Entries,Afficher les entrées de retour, Show Salary Slip,Afficher la Fiche de Salaire, @@ -2827,7 +2827,7 @@ Silt,Limon, Single Variant,Variante unique, Single unit of an Item.,Seule unité d'un Article., "Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}","Attribution des congés de congé pour les employés suivants, car des dossiers de répartition des congés existent déjà contre eux. {0}", -"Skipping Salary Structure Assignment for the following employees, as Salary Structure Assignment records already exists against them. {0}","Ignorer l'affectation de structure salariale pour les employés suivants, car des enregistrements d'affectation de structure salariale existent déjà pour eux. {0}", +"Skipping Salary Structure Assignment for the following employees, as Salary Structure Assignment records already exists against them. {0}","Ignorer l'affectation de structure salariale pour les employés suivants, car des enregistrements d'affectation de structure salariale existent déjà pour eux. {0}", Slideshow,Diaporama, Slots for {0} are not added to the schedule,Les créneaux pour {0} ne sont pas ajoutés à l'agenda, Small,Petit, @@ -2860,9 +2860,9 @@ Standard Buying,Achat standard, Standard Selling,Vente standard, Standard contract terms for Sales or Purchase.,Termes contractuels standards pour Ventes ou Achats, Start Date,Date de début, -Start Date of Agreement can't be greater than or equal to End Date.,La date de début de l'accord ne peut être supérieure ou égale à la date de fin., +Start Date of Agreement can't be greater than or equal to End Date.,La date de début de l'accord ne peut être supérieure ou égale à la date de fin., Start Year,Année de début, -"Start and end dates not in a valid Payroll Period, cannot calculate {0}","Les dates de début et de fin ne faisant pas partie d'une période de paie valide, impossible de calculer {0}", +"Start and end dates not in a valid Payroll Period, cannot calculate {0}","Les dates de début et de fin ne faisant pas partie d'une période de paie valide, impossible de calculer {0}", "Start and end dates not in a valid Payroll Period, cannot calculate {0}.","Les dates de début et de fin ne figurant pas dans une période de paie valide, le système ne peut pas calculer {0}.", Start date should be less than end date for Item {0},La date de début doit être antérieure à la date de fin pour l'Article {0}, Start date should be less than end date for task {0},La date de début doit être inférieure à la date de fin de la tâche {0}, @@ -2919,17 +2919,17 @@ Student Group,Groupe Étudiant, Student Group Strength,Force du Groupe d'Étudiant, Student Group is already updated.,Le Groupe d'Étudiants est déjà mis à jour., Student Group or Course Schedule is mandatory,Le Ggroupe d'Étudiants ou le Calendrier des Cours est obligatoire, -Student Group: ,Groupe d'étudiants:, +Student Group: ,Groupe d'étudiants:, Student ID,Carte d'Étudiant, -Student ID: ,Carte d'étudiant:, +Student ID: ,Carte d'étudiant:, Student LMS Activity,Activité LMS des étudiants, Student Mobile No.,N° de Mobile de l'Étudiant, Student Name,Nom de l'Étudiant, -Student Name: ,Nom d'étudiant:, +Student Name: ,Nom d'étudiant:, Student Report Card,Carte d'étudiant, Student is already enrolled.,L'étudiant est déjà inscrit., Student {0} - {1} appears Multiple times in row {2} & {3},Étudiant {0} - {1} apparaît Plusieurs fois dans la ligne {2} & {3}, -Student {0} does not belong to group {1},L'élève {0} n'appartient pas au groupe {1}, +Student {0} does not belong to group {1},L'élève {0} n'appartient pas au groupe {1}, Student {0} exist against student applicant {1},Étudiant {0} existe pour la candidature d'un étudiant {1}, "Students are at the heart of the system, add all your students","Les étudiants sont au cœur du système, ajouter tous vos étudiants", Sub Assemblies,Sous-Ensembles, @@ -2977,10 +2977,10 @@ Supplier Warehouse mandatory for sub-contracted Purchase Receipt,Entrepôt Fourn Supplier database.,Base de données fournisseurs., Supplier {0} not found in {1},Fournisseur {0} introuvable dans {1}, Supplier(s),Fournisseur(s), -Supplies made to UIN holders,Fournitures faites aux titulaires de l'UIN, +Supplies made to UIN holders,Fournitures faites aux titulaires de l'UIN, Supplies made to Unregistered Persons,Fournitures faites à des personnes non inscrites, Suppliies made to Composition Taxable Persons,Suppleies à des personnes assujetties à la composition, -Supply Type,Type d'approvisionnement, +Supply Type,Type d'approvisionnement, Support,Soutien, Support Analytics,Analyse du Support, Support Settings,Paramètres du Support, @@ -3014,9 +3014,9 @@ Tax Rate,Taux d'Imposition, Tax Rule Conflicts with {0},Règle de Taxation est en Conflit avec {0}, Tax Rule for transactions.,Règle de Taxation pour les transactions., Tax Template is mandatory.,Un Modèle de Taxe est obligatoire., -Tax Withholding rates to be applied on transactions.,Taux de retenue d'impôt à appliquer aux transactions., +Tax Withholding rates to be applied on transactions.,Taux de retenue d'impôt à appliquer aux transactions., Tax template for buying transactions.,Modèle de taxe pour les opérations d’achat., -Tax template for item tax rates.,Modèle de taxe pour les taux de taxe d'article., +Tax template for item tax rates.,Modèle de taxe pour les taux de taxe d'article., Tax template for selling transactions.,Modèle de taxe pour les opérations de vente., Taxable Amount,Montant Taxable, Taxes,Taxes, @@ -3044,7 +3044,7 @@ Thank you for your business!,Merci pour votre entreprise !, The 'From Package No.' field must neither be empty nor it's value less than 1.,Le champ 'N° de Paquet' ne doit pas être vide ni sa valeur être inférieure à 1., The Brand,La marque, The Item {0} cannot have Batch,L'Article {0} ne peut être en Lot, -The Loyalty Program isn't valid for the selected company,Le programme de fidélité n'est pas valable pour la société sélectionnée, +The Loyalty Program isn't valid for the selected company,Le programme de fidélité n'est pas valable pour la société sélectionnée, The Payment Term at row {0} is possibly a duplicate.,Le délai de paiement à la ligne {0} est probablement un doublon., The Term End Date cannot be earlier than the Term Start Date. Please correct the dates and try again.,La Date de Fin de Terme ne peut pas être antérieure à la Date de Début de Terme. Veuillez corriger les dates et essayer à nouveau., 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.,La Date de Fin de Terme ne peut pas être postérieure à la Date de Fin de l'Année Académique à laquelle le terme est lié (Année Académique {}). Veuillez corriger les dates et essayer à nouveau., @@ -3064,21 +3064,21 @@ The payment gateway account in plan {0} is different from the payment gateway ac The request for quotation can be accessed by clicking on the following link,La demande de devis peut être consultée en cliquant sur le lien suivant, The selected BOMs are not for the same item,Les LDMs sélectionnées ne sont pas pour le même article, The selected item cannot have Batch,L’article sélectionné ne peut pas avoir de Lot, -The seller and the buyer cannot be the same,Le vendeur et l'acheteur ne peuvent pas être les mêmes, -The shareholder does not belong to this company,L'actionnaire n'appartient pas à cette société, +The seller and the buyer cannot be the same,Le vendeur et l'acheteur ne peuvent pas être les mêmes, +The shareholder does not belong to this company,L'actionnaire n'appartient pas à cette société, The shares already exist,Les actions existent déjà, The shares don't exist with the {0},Les actions n'existent pas pour {0}, -"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","La tâche a été mise en file d'attente en tant que tâche en arrière-plan. En cas de problème de traitement en arrière-plan, le système ajoute un commentaire concernant l'erreur sur ce rapprochement des stocks et revient au stade de brouillon.", +"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","La tâche a été mise en file d'attente en tant que tâche en arrière-plan. En cas de problème de traitement en arrière-plan, le système ajoute un commentaire concernant l'erreur sur ce rapprochement des stocks et revient au stade de brouillon.", "Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.","Les Règles de Tarification sont ensuite filtrées en fonction des Clients, des Groupes de Clients, des Régions, des Fournisseurs, des Groupes de Fournisseurs, des Campagnes, des Partenaires Commerciaux, etc.", "There are inconsistencies between the rate, no of shares and the amount calculated","Il existe des incohérences entre le prix unitaire, le nombre d'actions et le montant calculé", There are more holidays than working days this month.,Il y a plus de vacances que de jours travaillés ce mois-ci., There can be multiple tiered collection factor based on the total spent. But the conversion factor for redemption will always be same for all the tier.,Il peut y avoir plusieurs facteurs de collecte hiérarchisés en fonction du total dépensé. Mais le facteur de conversion pour l'échange sera toujours le même pour tous les niveaux., There can only be 1 Account per Company in {0} {1},Il ne peut y avoir qu’un Compte par Société dans {0} {1}, "There can only be one Shipping Rule Condition with 0 or blank value for ""To Value""","Il ne peut y avoir qu’une Condition de Règle de Livraison avec 0 ou une valeur vide pour « A la Valeur""", -There is no leave period in between {0} and {1},Il n'y a pas de période de congé entre {0} et {1}, +There is no leave period in between {0} and {1},Il n'y a pas de période de congé entre {0} et {1}, There is not enough leave balance for Leave Type {0},Il n'y a pas assez de solde de congés pour les Congés de Type {0}, There is nothing to edit.,Il n'y a rien à modifier., -There isn't any item variant for the selected item,Il n'y a pas de variante d'article pour l'article sélectionné, +There isn't any item variant for the selected item,Il n'y a pas de variante d'article pour l'article sélectionné, "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.","Il semble y avoir un problème avec la configuration de GoCardless sur le serveur. Ne vous inquiétez pas, en cas d'échec, le montant sera remboursé sur votre compte.", There were errors creating Course Schedule,Des erreurs se sont produites lors de la création du programme, There were errors.,Il y a eu des erreurs., @@ -3108,7 +3108,7 @@ This is based on transactions against this Healthcare Practitioner.,Ce graphique This is based on transactions against this Patient. See timeline below for details,Ceci est basé sur les transactions de ce patient. Voir la chronologie ci-dessous pour plus de détails, This is based on transactions against this Sales Person. See timeline below for details,Ceci est basé sur les transactions contre ce vendeur. Voir la chronologie ci-dessous pour plus de détails, This is based on transactions against this Supplier. See timeline below for details,Basé sur les transactions avec ce fournisseur. Voir la chronologie ci-dessous pour plus de détails, -This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?,Cela permettra de soumettre des bulletins de salaire et de créer une écriture de journal d'accumulation. Voulez-vous poursuivre?, +This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?,Cela permettra de soumettre des bulletins de salaire et de créer une écriture de journal d'accumulation. Voulez-vous poursuivre?, This {0} conflicts with {1} for {2} {3},Ce {0} est en conflit avec {1} pour {2} {3}, Time Sheet for manufacturing.,Feuille de Temps pour la production., Time Tracking,Suivi du temps, @@ -3135,7 +3135,7 @@ To Date should be within the Fiscal Year. Assuming To Date = {0},La Date Finale To Datetime,À la Date, To Deliver,À Livrer, To Deliver and Bill,À Livrer et Facturer, -To Fiscal Year,À l'année fiscale, +To Fiscal Year,À l'année fiscale, To GSTIN,GSTIN (Destination), To Party Name,Nom du tiers (Destination), To Pin Code,Code postal (Destination), @@ -3151,7 +3151,7 @@ To date can not greater than employee's relieving date,La date de fin ne peut pa "To filter based on Party, select Party Type first","Pour filtrer en fonction du Tiers, sélectionnez d’abord le Type de Tiers", "To get the best out of ERPNext, we recommend that you take some time and watch these help videos.","Pour tirer le meilleur parti d’ERPNext, nous vous recommandons de prendre un peu de temps et de regarder ces vidéos d'aide.", "To include tax in row {0} in Item rate, taxes in rows {1} must also be included","Pour inclure la taxe de la ligne {0} dans le prix de l'Article, les taxes des lignes {1} doivent également être incluses", -To make Customer based incentive schemes.,Faire des programmes d'incitation basés sur le client., +To make Customer based incentive schemes.,Faire des programmes d'incitation basés sur le client., "To merge, following properties must be same for both items","Pour fusionner, les propriétés suivantes doivent être les mêmes pour les deux articles", "To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.","Pour ne pas appliquer la Règle de Tarification dans une transaction particulière, toutes les Règles de Tarification applicables doivent être désactivées.", "To set this Fiscal Year as Default, click on 'Set as Default'","Pour définir cet Exercice Fiscal par défaut, cliquez sur ""Définir par défaut""", @@ -3202,9 +3202,9 @@ Total Unpaid: {0},Total des Impayés : {0}, Total Variance,Variance totale, Total Weightage of all Assessment Criteria must be 100%,Le total des pondérations de tous les Critères d'Évaluation doit être égal à 100%, Total advance ({0}) against Order {1} cannot be greater than the Grand Total ({2}),Avance totale ({0}) pour la Commande {1} ne peut pas être supérieure au Total Général ({2}), -Total advance amount cannot be greater than total claimed amount,Le montant total de l'avance ne peut être supérieur au montant total réclamé, +Total advance amount cannot be greater than total claimed amount,Le montant total de l'avance ne peut être supérieur au montant total réclamé, Total advance amount cannot be greater than total sanctioned amount,Le montant total de l'avance ne peut être supérieur au montant total approuvé, -Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period,Le nombre total de congés alloués est supérieur de plusieurs jours à l'allocation maximale du type de congé {0} pour l'employé {1} au cours de la période, +Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period,Le nombre total de congés alloués est supérieur de plusieurs jours à l'allocation maximale du type de congé {0} pour l'employé {1} au cours de la période, Total allocated leaves are more than days in the period,Le Total des feuilles attribuées est supérieur au nombre de jours dans la période, Total allocated percentage for sales team should be 100,Pourcentage total attribué à l'équipe commerciale devrait être de 100, Total cannot be zero,Total ne peut pas être zéro, @@ -3303,18 +3303,18 @@ User Forum,Forum de l'Utilisateur, User ID,ID de l'Utilisateur, User ID not set for Employee {0},ID de l'Utilisateur non défini pour l'Employé {0}, User Remark,Remarque de l'Utilisateur, -User has not applied rule on the invoice {0},L'utilisateur n'a pas appliqué la règle sur la facture {0}, -User {0} already exists,L'utilisateur {0} existe déjà, +User has not applied rule on the invoice {0},L'utilisateur n'a pas appliqué la règle sur la facture {0}, +User {0} already exists,L'utilisateur {0} existe déjà, User {0} created,Utilisateur {0} créé, User {0} does not exist,Utilisateur {0} n'existe pas, -User {0} doesn't have any default POS Profile. Check Default at Row {1} for this User.,L'utilisateur {0} n'a aucun profil POS par défaut. Vérifiez par défaut à la ligne {1} pour cet utilisateur., +User {0} doesn't have any default POS Profile. Check Default at Row {1} for this User.,L'utilisateur {0} n'a aucun profil POS par défaut. Vérifiez par défaut à la ligne {1} pour cet utilisateur., User {0} is already assigned to Employee {1},Utilisateur {0} est déjà attribué à l'Employé {1}, -User {0} is already assigned to Healthcare Practitioner {1},L'utilisateur {0} est déjà attribué à un professionnel de la santé {1}, +User {0} is already assigned to Healthcare Practitioner {1},L'utilisateur {0} est déjà attribué à un professionnel de la santé {1}, Users,Utilisateurs, Utility Expenses,Frais de Services d'Utilité Publique, Valid From Date must be lesser than Valid Upto Date.,La date de début de validité doit être inférieure à la date de mise en service valide., Valid Till,Valable Jusqu'au, -Valid from and valid upto fields are mandatory for the cumulative,Les champs valides à partir de et valables jusqu'à sont obligatoires pour le cumulatif., +Valid from and valid upto fields are mandatory for the cumulative,Les champs valides à partir de et valables jusqu'à sont obligatoires pour le cumulatif., Valid from date must be less than valid upto date,La date de début de validité doit être inférieure à la date de validité, Valid till date cannot be before transaction date,La date de validité ne peut pas être avant la date de transaction, Validity,Validité, @@ -3327,7 +3327,7 @@ Value Proposition,Proposition de valeur, Value for Attribute {0} must be within the range of {1} to {2} in the increments of {3} for Item {4},Valeur pour l'attribut {0} doit être dans la gamme de {1} à {2} dans les incréments de {3} pour le poste {4}, Value missing,Valeur manquante, Value must be between {0} and {1},La valeur doit être comprise entre {0} et {1}., -"Values of exempt, nil rated and non-GST inward supplies","Valeurs des fournitures importées exonérées, assorties d'une cote zéro et non liées à la TPS", +"Values of exempt, nil rated and non-GST inward supplies","Valeurs des fournitures importées exonérées, assorties d'une cote zéro et non liées à la TPS", Variable,Variable, Variance,Variance, Variance ({}),Variance ({}), @@ -3335,7 +3335,7 @@ Variant,Variante, Variant Attributes,Attributs Variant, Variant Based On cannot be changed,Les variantes basées sur ne peuvent pas être modifiées, Variant Details Report,Rapport détaillé des variantes, -Variant creation has been queued.,La création de variantes a été placée en file d'attente., +Variant creation has been queued.,La création de variantes a été placée en file d'attente., Vehicle Expenses,Frais de véhicule, Vehicle No,N° du Véhicule, Vehicle Type,Type de véhicule, @@ -3354,7 +3354,7 @@ Visit report for maintenance call.,Rapport de visite pour appel de maintenance, Visit the forums,Visitez les forums, Vital Signs,Signes vitaux, Volunteer,Bénévole, -Volunteer Type information.,Volontaire Type d'information., +Volunteer Type information.,Volontaire Type d'information., Volunteer information.,Informations sur le bénévolat, Voucher #,Référence #, Voucher No,N° de Référence, @@ -3367,7 +3367,7 @@ Warehouse is mandatory,L'entrepôt est obligatoire, Warehouse is mandatory for stock Item {0} in row {1},L’entrepôt est obligatoire pour l'Article du stock {0} dans la ligne {1}, Warehouse not found in the system,L'entrepôt n'a pas été trouvé dans le système, "Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}",Entrepôt requis à la ligne n ° {0}. Veuillez définir un entrepôt par défaut pour l'article {1} et la société {2}, -Warehouse required for stock Item {0},Magasin requis pour l'article en stock {0}, +Warehouse required for stock Item {0},Magasin requis pour l'article en stock {0}, Warehouse {0} can not be deleted as quantity exists for Item {1},L'entrepôt {0} ne peut pas être supprimé car il existe une quantité pour l'Article {1}, Warehouse {0} does not belong to company {1},L'entrepôt {0} n'appartient pas à la société {1}, Warehouse {0} does not exist,L'entrepôt {0} n'existe pas, @@ -3442,13 +3442,13 @@ You cannot credit and debit same account at the same time,Vous ne pouvez pas cr You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings,Vous ne pouvez pas supprimer l'exercice fiscal {0}. L'exercice fiscal {0} est défini par défaut dans les Paramètres Globaux, You cannot delete Project Type 'External',Vous ne pouvez pas supprimer le Type de Projet 'Externe', You cannot edit root node.,Vous ne pouvez pas modifier le nœud racine., -You cannot restart a Subscription that is not cancelled.,Vous ne pouvez pas redémarrer un abonnement qui n'est pas annulé., +You cannot restart a Subscription that is not cancelled.,Vous ne pouvez pas redémarrer un abonnement qui n'est pas annulé., You don't have enought Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger, -You have already assessed for the assessment criteria {}.,Vous avez déjà évalué les critères d'évaluation {}., +You have already assessed for the assessment criteria {}.,Vous avez déjà évalué les critères d'évaluation {}., You have already selected items from {0} {1},Vous avez déjà choisi des articles de {0} {1}, You have been invited to collaborate on the project: {0},Vous avez été invité à collaborer sur le projet : {0}, You have entered duplicate items. Please rectify and try again.,Vous avez entré un doublon. Veuillez rectifier et essayer à nouveau., -You need to be a user other than Administrator with System Manager and Item Manager roles to register on Marketplace.,Vous devez être un utilisateur autre que l'administrateur avec les rôles System Manager et Item Manager pour vous inscrire sur Marketplace., +You need to be a user other than Administrator with System Manager and Item Manager roles to register on Marketplace.,Vous devez être un utilisateur autre que l'administrateur avec les rôles System Manager et Item Manager pour vous inscrire sur Marketplace., You need to be a user with System Manager and Item Manager roles to add users to Marketplace.,Vous devez être un utilisateur doté de rôles System Manager et Item Manager pour ajouter des utilisateurs à Marketplace., You need to be a user with System Manager and Item Manager roles to register on Marketplace.,Vous devez être un utilisateur avec des rôles System Manager et Item Manager pour vous inscrire sur Marketplace., You need to be logged in to access this page,Vous devez être connecté pour pouvoir accéder à cette page, @@ -3512,11 +3512,11 @@ on,sur, {0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.,{0} est obligatoire. Peut-être qu’un enregistrement de Taux de Change n'est pas créé pour {1} et {2}., {0} is not a stock Item,{0} n'est pas un Article de stock, {0} is not a valid Batch Number for Item {1},{0} n'est pas un Numéro de Lot valide pour l’Article {1}, -{0} is not added in the table,{0} n'est pas ajouté dans la table, -{0} is not in Optional Holiday List,{0} n'est pas dans la liste des jours fériés facultatifs, -{0} is not in a valid Payroll Period,{0} n'est pas dans une période de paie valide, +{0} is not added in the table,{0} n'est pas ajouté dans la table, +{0} is not in Optional Holiday List,{0} n'est pas dans la liste des jours fériés facultatifs, +{0} is not in a valid Payroll Period,{0} n'est pas dans une période de paie valide, {0} is now the default Fiscal Year. Please refresh your browser for the change to take effect.,{0} est désormais l’Exercice par défaut. Veuillez actualiser la page pour que les modifications soient prises en compte., -{0} is on hold till {1},{0} est en attente jusqu'à {1}, +{0} is on hold till {1},{0} est en attente jusqu'à {1}, {0} item found.,{0} élément trouvé., {0} items found.,{0} éléments trouvés., {0} items in progress,{0} articles en cours, @@ -3525,8 +3525,8 @@ on,sur, {0} must be negative in return document,{0} doit être négatif dans le document de retour, {0} must be submitted,{0} doit être soumis, {0} not allowed to transact with {1}. Please change the Company.,{0} n'est pas autorisé à traiter avec {1}. Veuillez changer la société., -{0} not found for item {1},{0} introuvable pour l'élément {1}, -{0} parameter is invalid,Le paramètre {0} n'est pas valide, +{0} not found for item {1},{0} introuvable pour l'élément {1}, +{0} parameter is invalid,Le paramètre {0} n'est pas valide, {0} payment entries can not be filtered by {1},{0} écritures de paiement ne peuvent pas être filtrées par {1}, {0} should be a value between 0 and 100,{0} devrait être une valeur comprise entre 0 et 100, {0} units of [{1}](#Form/Item/{1}) found in [{2}](#Form/Warehouse/{2}),{0} unités de [{1}] (#Formulaire/Article/{1}) trouvées dans [{2}] (#Formulaire/Entrepôt/{2}), @@ -3536,7 +3536,7 @@ on,sur, {0} variants created.,{0} variantes créées., {0} {1} created,{0} {1} créé, {0} {1} does not exist,{0} {1} n'existe pas, -{0} {1} does not exist.,{0} {1} n'existe pas, +{0} {1} does not exist.,{0} {1} n'existe pas, {0} {1} has been modified. Please refresh.,{0} {1} a été modifié. Veuillez actualiser., {0} {1} has not been submitted so the action cannot be completed,"{0} {1} n'a pas été soumis, donc l'action ne peut pas être complétée", "{0} {1} is associated with {2}, but Party Account is {3}","{0} {1} est associé à {2}, mais le compte tiers est {3}", @@ -3548,8 +3548,8 @@ on,sur, {0} {1} is frozen,{0} {1} est gelée, {0} {1} is fully billed,{0} {1} est entièrement facturé, {0} {1} is not active,{0} {1} n'est pas actif, -{0} {1} is not associated with {2} {3},{0} {1} n'est pas associé à {2} {3}, -{0} {1} is not present in the parent company,{0} {1} n'est pas présent dans la société mère, +{0} {1} is not associated with {2} {3},{0} {1} n'est pas associé à {2} {3}, +{0} {1} is not present in the parent company,{0} {1} n'est pas présent dans la société mère, {0} {1} is not submitted,{0} {1} n'a pas été soumis, {0} {1} is {2},{0} {1} est {2}, {0} {1} must be submitted,{0} {1} doit être soumis, @@ -3608,28 +3608,28 @@ or,ou, Ageing Range 4,Gamme de vieillissement 4, Allocated amount cannot be greater than unadjusted amount,Le montant alloué ne peut être supérieur au montant non ajusté, Allocated amount cannot be negative,Le montant alloué ne peut être négatif, -"Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry","Le compte d'écart doit être un compte de type actif / passif, car cette entrée de stock est une entrée d'ouverture.", +"Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry","Le compte d'écart doit être un compte de type actif / passif, car cette entrée de stock est une entrée d'ouverture.", Error in some rows,Erreur dans certaines lignes, Import Successful,Importation réussie, -Please save first,S'il vous plaît enregistrer en premier, -Price not found for item {0} in price list {1},Prix non trouvé pour l'article {0} dans la liste de prix {1}, -Warehouse Type,Type d'entrepôt, -'Date' is required,'Date' est requis, +Please save first,S'il vous plaît enregistrer en premier, +Price not found for item {0} in price list {1},Prix non trouvé pour l'article {0} dans la liste de prix {1}, +Warehouse Type,Type d'entrepôt, +'Date' is required,'Date' est requis, Benefit,Avantage, Budgets,Budgets, Bundle Qty,Quantité de paquet, Company GSTIN,GSTIN de la Société, -Company field is required,Le champ de l'entreprise est obligatoire, +Company field is required,Le champ de l'entreprise est obligatoire, Creating Dimensions...,Créer des dimensions ..., Duplicate entry against the item code {0} and manufacturer {1},Dupliquer la saisie par rapport au code article {0} et au fabricant {1}, Import Chart Of Accounts from CSV / Excel files,Importer un graphique des comptes à partir de fichiers CSV / Excel, -Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers,GSTIN invalide! L'entrée que vous avez entrée ne correspond pas au format GSTIN pour les titulaires d'un UIN ou les fournisseurs de services OIDAR non résidents, +Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers,GSTIN invalide! L'entrée que vous avez entrée ne correspond pas au format GSTIN pour les titulaires d'un UIN ou les fournisseurs de services OIDAR non résidents, Invoice Grand Total,Total général de la facture, Last carbon check date cannot be a future date,La date du dernier bilan carbone ne peut pas être une date future, Make Stock Entry,Faire une entrée de stock, Quality Feedback,Commentaires sur la qualité, Quality Feedback Template,Modèle de commentaires sur la qualité, -Rules for applying different promotional schemes.,Règles d'application de différents programmes promotionnels., +Rules for applying different promotional schemes.,Règles d'application de différents programmes promotionnels., Shift,Décalage, Show {0},Montrer {0}, "Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Caractères spéciaux sauf "-", "#", ".", "/", "{" Et "}" non autorisés dans les séries de nommage", @@ -3649,7 +3649,7 @@ No data to export,Aucune donnée à exporter, Print Heading,Imprimer Titre, Video,Vidéo, % Of Grand Total,% Du grand total, -'employee_field_value' and 'timestamp' are required.,'employee_field_value' et 'timestamp' sont obligatoires., +'employee_field_value' and 'timestamp' are required.,'employee_field_value' et 'timestamp' sont obligatoires., Company is a mandatory filter.,La société est un filtre obligatoire., From Date is a mandatory filter.,De la date est un filtre obligatoire., From Time cannot be later than To Time for {0},From Time ne peut pas être postérieur à To Time pour {0}, @@ -3657,13 +3657,13 @@ Video,Vidéo, A new appointment has been created for you with {0},Un nouveau rendez-vous a été créé pour vous avec {0}, Account Value,Valeur du compte, Account is mandatory to get payment entries,Le compte est obligatoire pour obtenir les entrées de paiement, -Account is not set for the dashboard chart {0},Le compte n'est pas défini pour le graphique du tableau de bord {0}, +Account is not set for the dashboard chart {0},Le compte n'est pas défini pour le graphique du tableau de bord {0}, Account {0} does not belong to company {1},Compte {0} n'appartient pas à la société {1}, -Account {0} does not exists in the dashboard chart {1},Le compte {0} n'existe pas dans le graphique du tableau de bord {1}, +Account {0} does not exists in the dashboard chart {1},Le compte {0} n'existe pas dans le graphique du tableau de bord {1}, Account: {0} is capital Work in progress and can not be updated by Journal Entry,Compte: {0} est un travail capital et ne peut pas être mis à jour par une écriture au journal., -Account: {0} is not permitted under Payment Entry,Compte: {0} n'est pas autorisé sous Saisie du paiement., +Account: {0} is not permitted under Payment Entry,Compte: {0} n'est pas autorisé sous Saisie du paiement., Accounting Dimension {0} is required for 'Balance Sheet' account {1}.,La dimension de comptabilité {0} est requise pour le compte "Bilan" {1}., -Accounting Dimension {0} is required for 'Profit and Loss' account {1}.,La dimension de comptabilité {0} est requise pour le compte 'Bénéfices et pertes' {1}., +Accounting Dimension {0} is required for 'Profit and Loss' account {1}.,La dimension de comptabilité {0} est requise pour le compte 'Bénéfices et pertes' {1}., Accounting Masters,Maîtres Comptables, Accounting Period overlaps with {0},La période comptable chevauche avec {0}, Activity,Activité, @@ -3672,14 +3672,14 @@ Add Child,Ajouter une Sous-Catégorie, Add Loan Security,Ajouter une garantie de prêt, Add Multiple,Ajout Multiple, Add Participants,Ajouter des participants, -Add to Featured Item,Ajouter à l'article en vedette, +Add to Featured Item,Ajouter à l'article en vedette, Add your review,Ajouter votre avis, Add/Edit Coupon Conditions,Ajouter / Modifier les conditions du coupon, Added to Featured Items,Ajouté aux articles en vedette, Added {0} ({1}),Ajouté {0} ({1}), Address Line 1,Adresse Ligne 1, Addresses,Adresses, -Admission End Date should be greater than Admission Start Date.,La date de fin d'admission doit être supérieure à la date de début d'admission., +Admission End Date should be greater than Admission Start Date.,La date de fin d'admission doit être supérieure à la date de début d'admission., Against Loan,Contre le prêt, Against Loan:,Contre le prêt:, All,Tout, @@ -3693,35 +3693,35 @@ Applied Coupon Code,Code de coupon appliqué, Apply Coupon Code,Appliquer le code de coupon, Appointment Booking,Prise de rendez-vous, "As there are existing transactions against item {0}, you can not change the value of {1}","Comme il existe des transactions avec l'article {0}, vous ne pouvez pas changer la valeur de {1}", -Asset Id,ID d'actif, -Asset Value,Valeur d'actif, -Asset Value Adjustment cannot be posted before Asset's purchase date {0}.,L'ajustement de la valeur de l'actif ne peut pas être enregistré avant la date d'achat de l'actif {0} ., -Asset {0} does not belongs to the custodian {1},L'élément {0} n'appartient pas au dépositaire {1}, -Asset {0} does not belongs to the location {1},L'élément {0} n'appartient pas à l'emplacement {1}, +Asset Id,ID d'actif, +Asset Value,Valeur d'actif, +Asset Value Adjustment cannot be posted before Asset's purchase date {0}.,L'ajustement de la valeur de l'actif ne peut pas être enregistré avant la date d'achat de l'actif {0} ., +Asset {0} does not belongs to the custodian {1},L'élément {0} n'appartient pas au dépositaire {1}, +Asset {0} does not belongs to the location {1},L'élément {0} n'appartient pas à l'emplacement {1}, At least one of the Applicable Modules should be selected,Au moins un des modules applicables doit être sélectionné, Atleast one asset has to be selected.,Au moins un actif doit être sélectionné., Attendance Marked,Présence marquée, Attendance has been marked as per employee check-ins,La présence a été marquée selon les enregistrements des employés, Authentication Failed,Authentification échouée, Automatic Reconciliation,Rapprochement automatique, -Available For Use Date,Date d'utilisation disponible, +Available For Use Date,Date d'utilisation disponible, Available Stock,Stock disponible, "Available quantity is {0}, you need {1}",La quantité disponible est {0}. Vous avez besoin de {1}., BOM 1,BOM 1, BOM 2,BOM 2, BOM Comparison Tool,Outil de comparaison de nomenclature, BOM recursion: {0} cannot be child of {1},Récursion de nomenclature: {0} ne peut pas être enfant de {1}, -BOM recursion: {0} cannot be parent or child of {1},Récursion de nomenclature: {0} ne peut pas être le parent ou l'enfant de {1}, +BOM recursion: {0} cannot be parent or child of {1},Récursion de nomenclature: {0} ne peut pas être le parent ou l'enfant de {1}, Back to Home,De retour à la maison, Back to Messages,Retour aux messages, -Bank Data mapper doesn't exist,Bank Data Mapper n'existe pas, +Bank Data mapper doesn't exist,Bank Data Mapper n'existe pas, Bank Details,Coordonnées bancaires, -Bank account '{0}' has been synchronized,Le compte bancaire '{0}' a été synchronisé, -Bank account {0} already exists and could not be created again,Le compte bancaire {0} existe déjà et n'a pas pu être créé à nouveau., +Bank account '{0}' has been synchronized,Le compte bancaire '{0}' a été synchronisé, +Bank account {0} already exists and could not be created again,Le compte bancaire {0} existe déjà et n'a pas pu être créé à nouveau., Bank accounts added,Comptes bancaires ajoutés, -Batch no is required for batched item {0},Le numéro de lot est requis pour l'article en lot {0}., +Batch no is required for batched item {0},Le numéro de lot est requis pour l'article en lot {0}., Billing Date,Date de facturation, -Billing Interval Count cannot be less than 1,Le nombre d'intervalles de facturation ne peut pas être inférieur à 1, +Billing Interval Count cannot be less than 1,Le nombre d'intervalles de facturation ne peut pas être inférieur à 1, Blue,Bleu, Book,Livre, Book Appointment,Prendre rendez-vous, @@ -3730,18 +3730,18 @@ Browse,Feuilleter, Call Connected,Appel connecté, Call Disconnected,Appel déconnecté, Call Missed,Appel manqué, -Call Summary,Résumé d'appel, -Call Summary Saved,Résumé de l'appel enregistré, +Call Summary,Résumé d'appel, +Call Summary Saved,Résumé de l'appel enregistré, Cancelled,Annulé, -Cannot Calculate Arrival Time as Driver Address is Missing.,Impossible de calculer l'heure d'arrivée car l'adresse du conducteur est manquante., -Cannot Optimize Route as Driver Address is Missing.,Impossible d'optimiser l'itinéraire car l'adresse du pilote est manquante., +Cannot Calculate Arrival Time as Driver Address is Missing.,Impossible de calculer l'heure d'arrivée car l'adresse du conducteur est manquante., +Cannot Optimize Route as Driver Address is Missing.,Impossible d'optimiser l'itinéraire car l'adresse du pilote est manquante., "Cannot Unpledge, loan security value is greater than the repaid amount","Impossible de désengager, la valeur de la garantie de prêt est supérieure au montant remboursé", -Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.,Impossible de terminer la tâche {0} car sa tâche dépendante {1} n'est pas terminée / annulée., -Cannot create loan until application is approved,Impossible de créer un prêt tant que la demande n'est pas approuvée, +Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.,Impossible de terminer la tâche {0} car sa tâche dépendante {1} n'est pas terminée / annulée., +Cannot create loan until application is approved,Impossible de créer un prêt tant que la demande n'est pas approuvée, Cannot find a matching Item. Please select some other value for {0}.,Impossible de trouver un article similaire. Veuillez sélectionner une autre valeur pour {0}., "Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings","La surfacturation pour le poste {0} dans la ligne {1} ne peut pas dépasser {2}. Pour autoriser la surfacturation, définissez la provision dans les paramètres du compte.", Cannot unpledge more than {0} qty of {0},Impossible de retirer plus de {0} quantité de {0}, -"Capacity Planning Error, planned start time can not be same as end time","Erreur de planification de capacité, l'heure de début prévue ne peut pas être identique à l'heure de fin", +"Capacity Planning Error, planned start time can not be same as end time","Erreur de planification de capacité, l'heure de début prévue ne peut pas être identique à l'heure de fin", Categories,Catégories, Changes in {0},Changements dans {0}, Chart,Graphique, @@ -3751,26 +3751,26 @@ Close,Fermer, Communication,la communication, Compact Item Print,Impression de l'Article Compacté, Company,Société, -Company of asset {0} and purchase document {1} doesn't matches.,La société de l'actif {0} et le document d'achat {1} ne correspondent pas., +Company of asset {0} and purchase document {1} doesn't matches.,La société de l'actif {0} et le document d'achat {1} ne correspondent pas., Compare BOMs for changes in Raw Materials and Operations,Comparer les nomenclatures aux modifications apportées aux matières premières et aux opérations, Compare List function takes on list arguments,La fonction de comparaison de liste accepte les arguments de liste, Complete,Terminé, Completed,Terminé, Completed Quantity,Quantité terminée, -Connect your Exotel Account to ERPNext and track call logs,Connectez votre compte Exotel à ERPNext et suivez les journaux d'appels, +Connect your Exotel Account to ERPNext and track call logs,Connectez votre compte Exotel à ERPNext et suivez les journaux d'appels, Connect your bank accounts to ERPNext,Connectez vos comptes bancaires à ERPNext, Contact Seller,Contacter le vendeur, Continue,Continuer, -Cost Center: {0} does not exist,Centre de coûts: {0} n'existe pas, +Cost Center: {0} does not exist,Centre de coûts: {0} n'existe pas, Couldn't Set Service Level Agreement {0}.,Impossible de définir le contrat de service {0}., Country,Pays, Country Code in File does not match with country code set up in the system,Le code de pays dans le fichier ne correspond pas au code de pays configuré dans le système, Create New Contact,Créer un nouveau contact, Create New Lead,Créer une nouvelle piste, Create Pick List,Créer une liste de choix, -Create Quality Inspection for Item {0},Créer un contrôle qualité pour l'article {0}, +Create Quality Inspection for Item {0},Créer un contrôle qualité pour l'article {0}, Creating Accounts...,Création de comptes ..., -Creating bank entries...,Création d'entrées bancaires ..., +Creating bank entries...,Création d'entrées bancaires ..., Creating {0},Création de {0}, Credit limit is already defined for the Company {0},La limite de crédit est déjà définie pour la société {0}., Ctrl + Enter to submit,Ctrl + Entrée pour soumettre, @@ -3782,7 +3782,7 @@ Customize,Personnaliser, Daily,Quotidien, Date,Date, Date Range,Intervalle de Date, -Date of Birth cannot be greater than Joining Date.,La date de naissance ne peut pas être supérieure à la date d'adhésion., +Date of Birth cannot be greater than Joining Date.,La date de naissance ne peut pas être supérieure à la date d'adhésion., Dear,Cher/Chère, Default,Par Défaut, Define coupon codes.,Définissez les codes promo., @@ -3808,40 +3808,40 @@ Due Date,Date d'Échéance, Duplicate,Dupliquer, Duplicate Project with Tasks,Projet en double avec tâches, Duplicate project has been created,Un projet en double a été créé, -E-Way Bill JSON can only be generated from a submitted document,E-Way Bill JSON ne peut être généré qu'à partir d'un document soumis, -E-Way Bill JSON can only be generated from submitted document,E-Way Bill JSON ne peut être généré qu'à partir du document soumis, +E-Way Bill JSON can only be generated from a submitted document,E-Way Bill JSON ne peut être généré qu'à partir d'un document soumis, +E-Way Bill JSON can only be generated from submitted document,E-Way Bill JSON ne peut être généré qu'à partir du document soumis, E-Way Bill JSON cannot be generated for Sales Return as of now,La facture e-Way JSON ne peut pas être générée pour le retour de vente à partir de maintenant, -ERPNext could not find any matching payment entry,ERPNext n'a trouvé aucune entrée de paiement correspondante, +ERPNext could not find any matching payment entry,ERPNext n'a trouvé aucune entrée de paiement correspondante, Earliest Age,Âge le plus précoce, Edit Details,Modifier les détails, Edit Profile,Modifier le Profil, -Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Un numéro d'identification de transporteur ou un numéro de véhicule est requis si le mode de transport est la route., +Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Un numéro d'identification de transporteur ou un numéro de véhicule est requis si le mode de transport est la route., Email,Email, Email Campaigns,Campagnes de courrier électronique, -Employee ID is linked with another instructor,L'ID de l'employé est lié à un autre instructeur, +Employee ID is linked with another instructor,L'ID de l'employé est lié à un autre instructeur, Employee Tax and Benefits,Impôt et avantages sociaux des employés, -Employee is required while issuing Asset {0},L'employé est requis lors de l'émission de l'actif {0}, -Employee {0} does not belongs to the company {1},L'employé {0} n'appartient pas à l'entreprise {1}, +Employee is required while issuing Asset {0},L'employé est requis lors de l'émission de l'actif {0}, +Employee {0} does not belongs to the company {1},L'employé {0} n'appartient pas à l'entreprise {1}, Enable Auto Re-Order,Activer la re-commande automatique, -End Date of Agreement can't be less than today.,La date de fin de l'accord ne peut être inférieure à celle d'aujourd'hui., +End Date of Agreement can't be less than today.,La date de fin de l'accord ne peut être inférieure à celle d'aujourd'hui., End Time,Heure de fin, Energy Point Leaderboard,Point de classement énergétique, Enter API key in Google Settings.,Entrez la clé API dans les paramètres Google., Enter Supplier,Entrez le fournisseur, Enter Value,Entrez une valeur, -Entity Type,Type d'entité, +Entity Type,Type d'entité, Error,Erreur, Error in Exotel incoming call,Erreur dans un appel entrant Exotel, Error: {0} is mandatory field,Erreur: {0} est un champ obligatoire, -Event Link,Lien d'événement, -Exception occurred while reconciling {0},Une exception s'est produite lors de la réconciliation {0}, -Expected and Discharge dates cannot be less than Admission Schedule date,Les dates prévues et de sortie ne peuvent pas être inférieures à la date du calendrier d'admission, -Expire Allocation,Expiration de l'allocation, +Event Link,Lien d'événement, +Exception occurred while reconciling {0},Une exception s'est produite lors de la réconciliation {0}, +Expected and Discharge dates cannot be less than Admission Schedule date,Les dates prévues et de sortie ne peuvent pas être inférieures à la date du calendrier d'admission, +Expire Allocation,Expiration de l'allocation, Expired,Expiré, Export,Exporter, Export not allowed. You need {0} role to export.,Pas autorisé à exporter. Vous devez avoir le rôle {0} pour exporter., -Failed to add Domain,Impossible d'ajouter le domaine, -Fetch Items from Warehouse,Récupérer des articles de l'entrepôt, +Failed to add Domain,Impossible d'ajouter le domaine, +Fetch Items from Warehouse,Récupérer des articles de l'entrepôt, Fetching...,Aller chercher..., Field,Champ, File Manager,Gestionnaire de fichiers, @@ -3852,13 +3852,13 @@ Finished Qty,Quantité finie, Fleet Management,Gestion de flotte, Following fields are mandatory to create address:,Les champs suivants sont obligatoires pour créer une adresse:, For Month,Pour mois, -"For item {0} at row {1}, count of serial numbers does not match with the picked quantity","Pour l'élément {0} à la ligne {1}, le nombre de numéros de série ne correspond pas à la quantité sélectionnée.", -For operation {0}: Quantity ({1}) can not be greter than pending quantity({2}),Pour l'opération {0}: la quantité ({1}) ne peut pas être supérieure à la quantité en attente ({2}), -For quantity {0} should not be greater than work order quantity {1},Pour la quantité {0} ne doit pas être supérieure à la quantité d'ordre de travail {1}, +"For item {0} at row {1}, count of serial numbers does not match with the picked quantity","Pour l'élément {0} à la ligne {1}, le nombre de numéros de série ne correspond pas à la quantité sélectionnée.", +For operation {0}: Quantity ({1}) can not be greter than pending quantity({2}),Pour l'opération {0}: la quantité ({1}) ne peut pas être supérieure à la quantité en attente ({2}), +For quantity {0} should not be greater than work order quantity {1},Pour la quantité {0} ne doit pas être supérieure à la quantité d'ordre de travail {1}, Free item not set in the pricing rule {0},Article gratuit non défini dans la règle de tarification {0}, From Date and To Date are Mandatory,La date de début et la date de fin sont obligatoires, From date can not be greater than than To date,La date de début ne peut être supérieure à la date de fin, -From employee is required while receiving Asset {0} to a target location,De l'employé est requis lors de la réception de l'actif {0} vers un emplacement cible, +From employee is required while receiving Asset {0} to a target location,De l'employé est requis lors de la réception de l'actif {0} vers un emplacement cible, Fuel Expense,Frais de carburant, Future Payment Amount,Montant du paiement futur, Future Payment Ref,Paiement futur Ref, @@ -3880,27 +3880,27 @@ Help Article,Article d’Aide, "Helps you keep tracks of Contracts based on Supplier, Customer and Employee","Vous aide à garder une trace des contrats en fonction du fournisseur, client et employé", Helps you manage appointments with your leads,Vous aide à gérer les rendez-vous avec vos prospects, Home,Accueil, -IBAN is not valid,IBAN n'est pas valide, +IBAN is not valid,IBAN n'est pas valide, Import Data from CSV / Excel files.,Importer des données à partir de fichiers CSV / Excel, In Progress,En cours, Incoming call from {0},Appel entrant du {0}, Incorrect Warehouse,Entrepôt incorrect, Interest Amount is mandatory,Le montant des intérêts est obligatoire, Intermediate,Intermédiaire, -Invalid Barcode. There is no Item attached to this barcode.,Code à barres invalide. Il n'y a pas d'article attaché à ce code à barres., -Invalid credentials,les informations d'identification invalides, +Invalid Barcode. There is no Item attached to this barcode.,Code à barres invalide. Il n'y a pas d'article attaché à ce code à barres., +Invalid credentials,les informations d'identification invalides, Invite as User,Inviter en tant qu'Utilisateur, -Issue Priority.,Priorité d'émission., +Issue Priority.,Priorité d'émission., Issue Type.,Type de probleme., "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.","Il semble qu'il y a un problème avec la configuration de Stripe sur le serveur. En cas d'erreur, le montant est remboursé sur votre compte.", Item Reported,Article rapporté, -Item listing removed,Liste d'articles supprimée, -Item quantity can not be zero,La quantité d'article ne peut être nulle, +Item listing removed,Liste d'articles supprimée, +Item quantity can not be zero,La quantité d'article ne peut être nulle, Item taxes updated,Taxes sur les articles mises à jour, Item {0}: {1} qty produced. ,Article {0}: {1} quantité produite., Items are required to pull the raw materials which is associated with it.,Les articles sont nécessaires pour extraire les matières premières qui lui sont associées., -Joining Date can not be greater than Leaving Date,La date d'adhésion ne peut pas être supérieure à la date de départ, -Lab Test Item {0} already exist,L'élément de test en laboratoire {0} existe déjà, +Joining Date can not be greater than Leaving Date,La date d'adhésion ne peut pas être supérieure à la date de départ, +Lab Test Item {0} already exist,L'élément de test en laboratoire {0} existe déjà, Last Issue,Dernier numéro, Latest Age,Dernier âge, Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay,La demande de congé est liée aux allocations de congé {0}. Demande de congé ne peut pas être défini comme congé sans solde, @@ -3910,7 +3910,7 @@ Liabilities,Passifs, Loading...,Chargement en Cours ..., Loan Amount exceeds maximum loan amount of {0} as per proposed securities,Le montant du prêt dépasse le montant maximal du prêt de {0} selon les titres proposés, Loan Applications from customers and employees.,Demandes de prêt des clients et des employés., -Loan Disbursement,Déboursement de l'emprunt, +Loan Disbursement,Déboursement de l'emprunt, Loan Processes,Processus de prêt, Loan Security,Sécurité des prêts, Loan Security Pledge,Garantie de prêt, @@ -3922,7 +3922,7 @@ Loan Security Price,Prix de la sécurité du prêt, Loan Security Price overlapping with {0},Le prix du titre de crédit se chevauche avec {0}, Loan Security Unpledge,Désengagement de garantie de prêt, Loan Security Value,Valeur de la sécurité du prêt, -Loan Type for interest and penalty rates,Type de prêt pour les taux d'intérêt et de pénalité, +Loan Type for interest and penalty rates,Type de prêt pour les taux d'intérêt et de pénalité, Loan amount cannot be greater than {0},Le montant du prêt ne peut pas être supérieur à {0}, Loan is mandatory,Le prêt est obligatoire, Loans,Les prêts, @@ -3951,17 +3951,17 @@ New Payment,Nouveau paiement, New release date should be in the future,La nouvelle date de sortie devrait être dans le futur, Newsletter,Newsletter, No Account matched these filters: {},Aucun compte ne correspond à ces filtres: {}, -No Employee found for the given employee field value. '{}': {},Aucun employé trouvé pour la valeur de champ d'employé donnée. '{}': {}, -No Leaves Allocated to Employee: {0} for Leave Type: {1},Aucun congé attribué à l'employé: {0} pour le type de congé: {1}, +No Employee found for the given employee field value. '{}': {},Aucun employé trouvé pour la valeur de champ d'employé donnée. '{}': {}, +No Leaves Allocated to Employee: {0} for Leave Type: {1},Aucun congé attribué à l'employé: {0} pour le type de congé: {1}, No communication found.,Aucune communication trouvée., -No correct answer is set for {0},Aucune réponse correcte n'est définie pour {0}., +No correct answer is set for {0},Aucune réponse correcte n'est définie pour {0}., No description,Pas de description, -No issue has been raised by the caller.,Aucun problème n'a été soulevé par l'appelant., +No issue has been raised by the caller.,Aucun problème n'a été soulevé par l'appelant., No items to publish,Aucun élément à publier, No outstanding invoices found,Aucune facture en attente trouvée, -No outstanding invoices found for the {0} {1} which qualify the filters you have specified.,Aucune facture en attente n'a été trouvée pour le {0} {1} qui qualifie les filtres que vous avez spécifiés., +No outstanding invoices found for the {0} {1} which qualify the filters you have specified.,Aucune facture en attente n'a été trouvée pour le {0} {1} qui qualifie les filtres que vous avez spécifiés., No outstanding invoices require exchange rate revaluation,Aucune facture en attente ne nécessite une réévaluation du taux de change, -No reviews yet,Pas encore d'avis, +No reviews yet,Pas encore d'avis, No views yet,Pas encore de vue, Non stock items,Articles hors stock, Not Allowed,Non Autorisé, @@ -3970,25 +3970,25 @@ Not permitted. Please disable the Lab Test Template,Pas permis. Veuillez désact Note,Note, Notes: ,Remarques :, Offline,Hors ligne, -On Converting Opportunity,Sur l'opportunité de conversion, +On Converting Opportunity,Sur l'opportunité de conversion, On Purchase Order Submission,Sur soumission de commande, On Sales Order Submission,Envoi de commande client, On Task Completion,En fin de tâche, On {0} Creation,Sur {0} Creation, Only .csv and .xlsx files are supported currently,Seuls les fichiers .csv et .xlsx sont actuellement pris en charge., -Only expired allocation can be cancelled,Seule l'allocation expirée peut être annulée, +Only expired allocation can be cancelled,Seule l'allocation expirée peut être annulée, Only users with the {0} role can create backdated leave applications,Seuls les utilisateurs avec le rôle {0} peuvent créer des demandes de congé antidatées, Open,Ouvert, Open Contact,Contact ouvert, Open Lead,Ouvrir le fil, Opening and Closing,Ouverture et fermeture, -Operating Cost as per Work Order / BOM,Coût d'exploitation selon l'ordre de travail / nomenclature, +Operating Cost as per Work Order / BOM,Coût d'exploitation selon l'ordre de travail / nomenclature, Order Amount,Montant de la commande, Page {0} of {1},Page {0} sur {1}, Paid amount cannot be less than {0},Le montant payé ne peut pas être inférieur à {0}, Parent Company must be a group company,La société mère doit être une société du groupe, Passing Score value should be between 0 and 100,La note de passage doit être comprise entre 0 et 100, -Password policy cannot contain spaces or simultaneous hyphens. The format will be restructured automatically,La politique de mot de passe ne peut pas contenir d'espaces ni de traits d'union simultanés. Le format sera restructuré automatiquement, +Password policy cannot contain spaces or simultaneous hyphens. The format will be restructured automatically,La politique de mot de passe ne peut pas contenir d'espaces ni de traits d'union simultanés. Le format sera restructuré automatiquement, Patient History,Histoire du patient, Pause,Pause, Pay,Payer, @@ -4001,39 +4001,39 @@ Period based On,Période basée sur, Perpetual inventory required for the company {0} to view this report.,Inventaire permanent requis pour que la société {0} puisse consulter ce rapport., Phone,Téléphone, Pick List,Liste de sélection, -Plaid authentication error,Erreur d'authentification du plaid, +Plaid authentication error,Erreur d'authentification du plaid, Plaid public token error,Erreur de jeton public Plaid, Plaid transactions sync error,Erreur de synchronisation des transactions plaid, -Please check the error log for details about the import errors,Veuillez consulter le journal des erreurs pour plus de détails sur les erreurs d'importation., +Please check the error log for details about the import errors,Veuillez consulter le journal des erreurs pour plus de détails sur les erreurs d'importation., Please click on the following link to set your new password,Veuillez cliquer sur le lien suivant pour définir votre nouveau mot de passe, -Please create DATEV Settings for Company {}.,Veuillez créer les paramètres DATEV pour l'entreprise {} ., -Please create adjustment Journal Entry for amount {0} ,Veuillez créer une écriture de journal d'ajustement pour le montant {0}, +Please create DATEV Settings for Company {}.,Veuillez créer les paramètres DATEV pour l'entreprise {} ., +Please create adjustment Journal Entry for amount {0} ,Veuillez créer une écriture de journal d'ajustement pour le montant {0}, Please do not create more than 500 items at a time,Ne créez pas plus de 500 objets à la fois., -Please enter Difference Account or set default Stock Adjustment Account for company {0},Veuillez saisir un compte d'écart ou définir un compte d'ajustement de stock par défaut pour la société {0}, -Please enter GSTIN and state for the Company Address {0},Veuillez saisir GSTIN et indiquer l'adresse de la société {0}., -Please enter Item Code to get item taxes,Veuillez entrer le code de l'article pour obtenir les taxes sur les articles, +Please enter Difference Account or set default Stock Adjustment Account for company {0},Veuillez saisir un compte d'écart ou définir un compte d'ajustement de stock par défaut pour la société {0}, +Please enter GSTIN and state for the Company Address {0},Veuillez saisir GSTIN et indiquer l'adresse de la société {0}., +Please enter Item Code to get item taxes,Veuillez entrer le code de l'article pour obtenir les taxes sur les articles, Please enter Warehouse and Date,Veuillez entrer entrepôt et date, -Please enter coupon code !!,S'il vous plaît entrer le code coupon !!, -Please enter the designation,S'il vous plaît entrer la désignation, +Please enter coupon code !!,S'il vous plaît entrer le code coupon !!, +Please enter the designation,S'il vous plaît entrer la désignation, Please enter valid coupon code !!,Veuillez entrer un code de coupon valide !!, -Please login as a Marketplace User to edit this item.,Veuillez vous connecter en tant qu'utilisateur Marketplace pour modifier cet article., -Please login as a Marketplace User to report this item.,Veuillez vous connecter en tant qu'utilisateur de la Marketplace pour signaler cet élément., +Please login as a Marketplace User to edit this item.,Veuillez vous connecter en tant qu'utilisateur Marketplace pour modifier cet article., +Please login as a Marketplace User to report this item.,Veuillez vous connecter en tant qu'utilisateur de la Marketplace pour signaler cet élément., Please select Template Type to download template,Veuillez sélectionner le type de modèle pour télécharger le modèle, -Please select Applicant Type first,Veuillez d'abord sélectionner le type de demandeur, -Please select Customer first,S'il vous plaît sélectionnez d'abord le client, -Please select Item Code first,Veuillez d'abord sélectionner le code d'article, +Please select Applicant Type first,Veuillez d'abord sélectionner le type de demandeur, +Please select Customer first,S'il vous plaît sélectionnez d'abord le client, +Please select Item Code first,Veuillez d'abord sélectionner le code d'article, Please select Loan Type for company {0},Veuillez sélectionner le type de prêt pour la société {0}, Please select a Delivery Note,Veuillez sélectionner un bon de livraison, -Please select a Sales Person for item: {0},Veuillez sélectionner un commercial pour l'article: {0}, +Please select a Sales Person for item: {0},Veuillez sélectionner un commercial pour l'article: {0}, Please select another payment method. Stripe does not support transactions in currency '{0}',Veuillez sélectionner une autre méthode de paiement. Stripe ne prend pas en charge les transactions en devise '{0}', -Please select the customer.,S'il vous plaît sélectionner le client., +Please select the customer.,S'il vous plaît sélectionner le client., Please set a Supplier against the Items to be considered in the Purchase Order.,Veuillez définir un fournisseur par rapport aux articles à prendre en compte dans le bon de commande., Please set account heads in GST Settings for Compnay {0},Définissez les en-têtes de compte dans les paramètres de la TPS pour le service {0}., Please set an email id for the Lead {0},Veuillez définir un identifiant de messagerie pour le prospect {0}., -Please set default UOM in Stock Settings,Veuillez définir l'UdM par défaut dans les paramètres de stock, -Please set filter based on Item or Warehouse due to a large amount of entries.,Veuillez définir le filtre en fonction de l'article ou de l'entrepôt en raison d'une grande quantité d'entrées., +Please set default UOM in Stock Settings,Veuillez définir l'UdM par défaut dans les paramètres de stock, +Please set filter based on Item or Warehouse due to a large amount of entries.,Veuillez définir le filtre en fonction de l'article ou de l'entrepôt en raison d'une grande quantité d'entrées., Please set up the Campaign Schedule in the Campaign {0},Configurez le calendrier de la campagne dans la campagne {0}., -Please set valid GSTIN No. in Company Address for company {0},Veuillez définir un numéro GSTIN valide dans l'adresse de l'entreprise pour l'entreprise {0}, +Please set valid GSTIN No. in Company Address for company {0},Veuillez définir un numéro GSTIN valide dans l'adresse de l'entreprise pour l'entreprise {0}, Please set {0},Veuillez définir {0},customer Please setup a default bank account for company {0},Veuillez configurer un compte bancaire par défaut pour la société {0}., Please specify,Veuillez spécifier, @@ -4048,29 +4048,29 @@ Processing XML Files,Traitement des fichiers XML, Profitability,Rentabilité, Project,Projet, Proposed Pledges are mandatory for secured Loans,Les engagements proposés sont obligatoires pour les prêts garantis, -Provide the academic year and set the starting and ending date.,Indiquez l'année universitaire et définissez la date de début et de fin., +Provide the academic year and set the starting and ending date.,Indiquez l'année universitaire et définissez la date de début et de fin., Public token is missing for this bank,Un jeton public est manquant pour cette banque, Publish,Publier, Publish 1 Item,Publier 1 élément, Publish Items,Publier des articles, -Publish More Items,Publier plus d'articles, +Publish More Items,Publier plus d'articles, Publish Your First Items,Publiez vos premiers articles, Publish {0} Items,Publier {0} éléments, Published Items,Articles publiés, -Purchase Invoice cannot be made against an existing asset {0},La facture d'achat ne peut pas être effectuée sur un élément existant {0}, -Purchase Invoices,Factures d'achat, +Purchase Invoice cannot be made against an existing asset {0},La facture d'achat ne peut pas être effectuée sur un élément existant {0}, +Purchase Invoices,Factures d'achat, Purchase Orders,Acheter en ligne, Purchase Receipt doesn't have any Item for which Retain Sample is enabled.,Le reçu d’achat ne contient aucun élément pour lequel Conserver échantillon est activé., Purchase Return,Retour d'Achat, Qty of Finished Goods Item,Quantité de produits finis, Qty or Amount is mandatroy for loan security,La quantité ou le montant est obligatoire pour la garantie de prêt, -Quality Inspection required for Item {0} to submit,Inspection de qualité requise pour que l'élément {0} soit envoyé, +Quality Inspection required for Item {0} to submit,Inspection de qualité requise pour que l'élément {0} soit envoyé, Quantity to Manufacture,Quantité à fabriquer, -Quantity to Manufacture can not be zero for the operation {0},La quantité à fabriquer ne peut pas être nulle pour l'opération {0}, +Quantity to Manufacture can not be zero for the operation {0},La quantité à fabriquer ne peut pas être nulle pour l'opération {0}, Quarterly,Trimestriel, Queued,File d'Attente, Quick Entry,Écriture Rapide, -Quiz {0} does not exist,Le questionnaire {0} n'existe pas, +Quiz {0} does not exist,Le questionnaire {0} n'existe pas, Quotation Amount,Montant du devis, Rate or Discount is required for the price discount.,Le taux ou la remise est requis pour la remise de prix., Reason,Raison, @@ -4081,7 +4081,7 @@ Recruitment,Recrutement, Red,rouge, Refreshing,Rafraîchissant, Release date must be in the future,La date de sortie doit être dans le futur, -Relieving Date must be greater than or equal to Date of Joining,La date de libération doit être supérieure ou égale à la date d'adhésion, +Relieving Date must be greater than or equal to Date of Joining,La date de libération doit être supérieure ou égale à la date d'adhésion, Rename,Renommer, Rename Not Allowed,Renommer non autorisé, Repayment Method is mandatory for term loans,La méthode de remboursement est obligatoire pour les prêts à terme, @@ -4090,70 +4090,70 @@ Report Item,Élément de rapport, Report this Item,Signaler cet article, Reserved Qty for Subcontract: Raw materials quantity to make subcontracted items.,Quantité réservée pour la sous-traitance: quantité de matières premières pour fabriquer des articles sous-traités., Reset,Réinitialiser, -Reset Service Level Agreement,Réinitialiser l'accord de niveau de service, -Resetting Service Level Agreement.,Réinitialisation de l'accord de niveau de service., -Response Time for {0} at index {1} can't be greater than Resolution Time.,Le temps de réponse pour {0} à l'index {1} ne peut pas être supérieur au temps de résolution., +Reset Service Level Agreement,Réinitialiser l'accord de niveau de service, +Resetting Service Level Agreement.,Réinitialisation de l'accord de niveau de service., +Response Time for {0} at index {1} can't be greater than Resolution Time.,Le temps de réponse pour {0} à l'index {1} ne peut pas être supérieur au temps de résolution., Return amount cannot be greater unclaimed amount,Le montant du retour ne peut pas être supérieur au montant non réclamé, Review,La revue, Room,Chambre, Room Type,Type de chambre, Row # ,Ligne #, -Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same,Ligne # {0}: l'entrepôt accepté et l'entrepôt fournisseur ne peuvent pas être identiques, -Row #{0}: Cannot delete item {1} which has already been billed.,Ligne # {0}: impossible de supprimer l'élément {1} qui a déjà été facturé., -Row #{0}: Cannot delete item {1} which has already been delivered,Ligne # {0}: impossible de supprimer l'élément {1} qui a déjà été livré, -Row #{0}: Cannot delete item {1} which has already been received,Ligne # {0}: impossible de supprimer l'élément {1} qui a déjà été reçu, -Row #{0}: Cannot delete item {1} which has work order assigned to it.,Ligne # {0}: impossible de supprimer l'élément {1} auquel un bon de travail est affecté., -Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.,Ligne # {0}: impossible de supprimer l'article {1} affecté à la commande d'achat du client., -Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor,Ligne # {0}: Impossible de sélectionner l'entrepôt fournisseur lors de la fourniture de matières premières au sous-traitant, -Row #{0}: Cost Center {1} does not belong to company {2},Ligne # {0}: le centre de coûts {1} n'appartient pas à l'entreprise {2}, -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}.,Ligne n ° {0}: l'opération {1} n'est pas terminée pour {2} quantité de produits finis dans l'ordre de travail {3}. Veuillez mettre à jour le statut de l'opération via la carte de travail {4}., +Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same,Ligne # {0}: l'entrepôt accepté et l'entrepôt fournisseur ne peuvent pas être identiques, +Row #{0}: Cannot delete item {1} which has already been billed.,Ligne # {0}: impossible de supprimer l'élément {1} qui a déjà été facturé., +Row #{0}: Cannot delete item {1} which has already been delivered,Ligne # {0}: impossible de supprimer l'élément {1} qui a déjà été livré, +Row #{0}: Cannot delete item {1} which has already been received,Ligne # {0}: impossible de supprimer l'élément {1} qui a déjà été reçu, +Row #{0}: Cannot delete item {1} which has work order assigned to it.,Ligne # {0}: impossible de supprimer l'élément {1} auquel un bon de travail est affecté., +Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.,Ligne # {0}: impossible de supprimer l'article {1} affecté à la commande d'achat du client., +Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor,Ligne # {0}: Impossible de sélectionner l'entrepôt fournisseur lors de la fourniture de matières premières au sous-traitant, +Row #{0}: Cost Center {1} does not belong to company {2},Ligne # {0}: le centre de coûts {1} n'appartient pas à l'entreprise {2}, +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}.,Ligne n ° {0}: l'opération {1} n'est pas terminée pour {2} quantité de produits finis dans l'ordre de travail {3}. Veuillez mettre à jour le statut de l'opération via la carte de travail {4}., Row #{0}: Payment document is required to complete the transaction,Ligne n ° {0}: Un document de paiement est requis pour effectuer la transaction., -Row #{0}: Serial No {1} does not belong to Batch {2},Ligne # {0}: le numéro de série {1} n'appartient pas au lot {2}, +Row #{0}: Serial No {1} does not belong to Batch {2},Ligne # {0}: le numéro de série {1} n'appartient pas au lot {2}, Row #{0}: Service End Date cannot be before Invoice Posting Date,Ligne # {0}: la date de fin du service ne peut pas être antérieure à la date de validation de la facture, Row #{0}: Service Start Date cannot be greater than Service End Date,Ligne # {0}: la date de début du service ne peut pas être supérieure à la date de fin du service, Row #{0}: Service Start and End Date is required for deferred accounting,Ligne # {0}: la date de début et de fin du service est requise pour la comptabilité différée, -Row {0}: Invalid Item Tax Template for item {1},Ligne {0}: modèle de taxe sur les articles non valide pour l'article {1}, -Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3}),Ligne {0}: quantité non disponible pour {4} dans l'entrepôt {1} au moment de la comptabilisation de l'entrée ({2} {3})., -Row {0}: user has not applied the rule {1} on the item {2},Ligne {0}: l'utilisateur n'a pas appliqué la règle {1} sur l'élément {2}, -Row {0}:Sibling Date of Birth cannot be greater than today.,Ligne {0}: la date de naissance du frère ou de la sœur ne peut pas être supérieure à celle d'aujourd'hui., +Row {0}: Invalid Item Tax Template for item {1},Ligne {0}: modèle de taxe sur les articles non valide pour l'article {1}, +Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3}),Ligne {0}: quantité non disponible pour {4} dans l'entrepôt {1} au moment de la comptabilisation de l'entrée ({2} {3})., +Row {0}: user has not applied the rule {1} on the item {2},Ligne {0}: l'utilisateur n'a pas appliqué la règle {1} sur l'élément {2}, +Row {0}:Sibling Date of Birth cannot be greater than today.,Ligne {0}: la date de naissance du frère ou de la sœur ne peut pas être supérieure à celle d'aujourd'hui., Row({0}): {1} is already discounted in {2},Ligne ({0}): {1} est déjà réduit dans {2}., Rows Added in {0},Lignes ajoutées dans {0}, Rows Removed in {0},Lignes supprimées dans {0}, Sanctioned Amount limit crossed for {0} {1},Montant sanctionné dépassé pour {0} {1}, -Sanctioned Loan Amount already exists for {0} against company {1},Le montant du prêt sanctionné existe déjà pour {0} contre l'entreprise {1}, +Sanctioned Loan Amount already exists for {0} against company {1},Le montant du prêt sanctionné existe déjà pour {0} contre l'entreprise {1}, Save,sauvegarder, -Save Item,Enregistrer l'élément, +Save Item,Enregistrer l'élément, Saved Items,Articles sauvegardés, -Scheduled and Admitted dates can not be less than today,Les dates prévues et admises ne peuvent être inférieures à celles d'aujourd'hui, +Scheduled and Admitted dates can not be less than today,Les dates prévues et admises ne peuvent être inférieures à celles d'aujourd'hui, Search Items ...,Rechercher des articles ..., Search for a payment,Rechercher un paiement, -Search for anything ...,Rechercher n'importe quoi ..., +Search for anything ...,Rechercher n'importe quoi ..., Search results for,Résultats de recherche pour, Select All,Sélectionner Tout, Select Difference Account,Sélectionnez compte différentiel, Select a Default Priority.,Sélectionnez une priorité par défaut., Select a Supplier from the Default Supplier List of the items below.,Sélectionnez un fournisseur dans la liste des fournisseurs par défaut des éléments ci-dessous., Select a company,Sélectionnez une entreprise, -Select finance book for the item {0} at row {1},Sélectionnez le livre de financement pour l'élément {0} à la ligne {1}., +Select finance book for the item {0} at row {1},Sélectionnez le livre de financement pour l'élément {0} à la ligne {1}., Select only one Priority as Default.,Sélectionnez une seule priorité par défaut., Seller Information,Information du vendeur, Send,Envoyer, Send a message,Envoyer un message, Sending,Envoi, -Sends Mails to lead or contact based on a Campaign schedule,Envoie des courriers à diriger ou à contacter en fonction d'un calendrier de campagne, +Sends Mails to lead or contact based on a Campaign schedule,Envoie des courriers à diriger ou à contacter en fonction d'un calendrier de campagne, Serial Number Created,Numéro de série créé, Serial Numbers Created,Numéros de série créés, -Serial no(s) required for serialized item {0},N ° de série requis pour l'article sérialisé {0}, +Serial no(s) required for serialized item {0},N ° de série requis pour l'article sérialisé {0}, Series,Séries, Server Error,erreur du serveur, -Service Level Agreement has been changed to {0}.,L'accord de niveau de service a été remplacé par {0}., -Service Level Agreement tracking is not enabled.,Le suivi des accords de niveau de service n'est pas activé., -Service Level Agreement was reset.,L'accord de niveau de service a été réinitialisé., -Service Level Agreement with Entity Type {0} and Entity {1} already exists.,L'accord de niveau de service avec le type d'entité {0} et l'entité {1} existe déjà., +Service Level Agreement has been changed to {0}.,L'accord de niveau de service a été remplacé par {0}., +Service Level Agreement tracking is not enabled.,Le suivi des accords de niveau de service n'est pas activé., +Service Level Agreement was reset.,L'accord de niveau de service a été réinitialisé., +Service Level Agreement with Entity Type {0} and Entity {1} already exists.,L'accord de niveau de service avec le type d'entité {0} et l'entité {1} existe déjà., Set,Définir, Set Meta Tags,Définir les balises méta, -Set Response Time and Resolution for Priority {0} at index {1}.,Définissez le temps de réponse et la résolution pour la priorité {0} à l'index {1}., -Set {0} in company {1},Définissez {0} dans l'entreprise {1}, +Set Response Time and Resolution for Priority {0} at index {1}.,Définissez le temps de réponse et la résolution pour la priorité {0} à l'index {1}., +Set {0} in company {1},Définissez {0} dans l'entreprise {1}, Setup,Configuration, Setup Wizard,Assistant de configuration, Shift Management,Gestion des quarts, @@ -4163,10 +4163,10 @@ Show Sales Person,Afficher le vendeur, Show Stock Ageing Data,Afficher les données sur le vieillissement des stocks, Show Warehouse-wise Stock,Afficher le stock entre les magasins, Size,Taille, -Something went wrong while evaluating the quiz.,Quelque chose s'est mal passé lors de l'évaluation du quiz., +Something went wrong while evaluating the quiz.,Quelque chose s'est mal passé lors de l'évaluation du quiz., "Sorry,coupon code are exhausted","Désolé, le code de coupon est épuisé", "Sorry,coupon code validity has expired","Désolé, la validité du code promo a expiré", -"Sorry,coupon code validity has not started","Désolé, la validité du code promo n'a pas commencé", +"Sorry,coupon code validity has not started","Désolé, la validité du code promo n'a pas commencé", Sr,Sr, Start,Démarrer, Start Date cannot be before the current date,La date de début ne peut pas être antérieure à la date du jour, @@ -4178,32 +4178,32 @@ Stock Entry has been already created against this Pick List,Une entrée de stock Stock Ledger ID,ID du registre des stocks, Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.,La valeur du stock ({0}) et le solde du compte ({1}) ne sont pas synchronisés pour le compte {2} et ses entrepôts liés., Stores - {0},Magasins - {0}, -Student with email {0} does not exist,Étudiant avec le courrier électronique {0} n'existe pas, +Student with email {0} does not exist,Étudiant avec le courrier électronique {0} n'existe pas, Submit Review,Poster un commentaire, Submitted,Soumis, Supplier Addresses And Contacts,Adresses et contacts des fournisseurs, Synchronize this account,Synchroniser ce compte, Tag,Étiquette, -Target Location is required while receiving Asset {0} from an employee,L'emplacement cible est requis lors de la réception de l'élément {0} d'un employé, -Target Location is required while transferring Asset {0},L'emplacement cible est requis lors du transfert de l'élément {0}, -Target Location or To Employee is required while receiving Asset {0},L'emplacement cible ou l'employé est requis lors de la réception de l'élément {0}, +Target Location is required while receiving Asset {0} from an employee,L'emplacement cible est requis lors de la réception de l'élément {0} d'un employé, +Target Location is required while transferring Asset {0},L'emplacement cible est requis lors du transfert de l'élément {0}, +Target Location or To Employee is required while receiving Asset {0},L'emplacement cible ou l'employé est requis lors de la réception de l'élément {0}, Task's {0} End Date cannot be after Project's End Date.,La date de fin {0} de la tâche ne peut pas être postérieure à la date de fin du projet., Task's {0} Start Date cannot be after Project's End Date.,La date de début {0} de la tâche ne peut pas être postérieure à la date de fin du projet., Tax Account not specified for Shopify Tax {0},Compte de taxe non spécifié pour Shopify Tax {0}, Tax Total,Total de la taxe, Template,Modèle, -The Campaign '{0}' already exists for the {1} '{2}',La campagne '{0}' existe déjà pour le {1} '{2}'., +The Campaign '{0}' already exists for the {1} '{2}',La campagne '{0}' existe déjà pour le {1} '{2}'., The difference between from time and To Time must be a multiple of Appointment,La différence entre from time et To Time doit être un multiple de Appointment, -The field Asset Account cannot be blank,Le champ Compte d'actif ne peut pas être vide, +The field Asset Account cannot be blank,Le champ Compte d'actif ne peut pas être vide, The field Equity/Liability Account cannot be blank,Le champ Compte d’équité / de responsabilité ne peut pas être vide, The following serial numbers were created:

    {0},Les numéros de série suivants ont été créés:

    {0}, -The parent account {0} does not exists in the uploaded template,Le compte parent {0} n'existe pas dans le modèle téléchargé, +The parent account {0} does not exists in the uploaded template,Le compte parent {0} n'existe pas dans le modèle téléchargé, The question cannot be duplicate,La question ne peut pas être dupliquée, -The selected payment entry should be linked with a creditor bank transaction,L'entrée de paiement sélectionnée doit être liée à une transaction bancaire créditrice, -The selected payment entry should be linked with a debtor bank transaction,L'entrée de paiement sélectionnée doit être liée à une transaction bancaire débitrice, +The selected payment entry should be linked with a creditor bank transaction,L'entrée de paiement sélectionnée doit être liée à une transaction bancaire créditrice, +The selected payment entry should be linked with a debtor bank transaction,L'entrée de paiement sélectionnée doit être liée à une transaction bancaire débitrice, The total allocated amount ({0}) is greated than the paid amount ({1}).,Le montant total alloué ({0}) est supérieur au montant payé ({1})., The value {0} is already assigned to an exisiting Item {2}.,La valeur {0} est déjà attribuée à un élément existant {2}., -There are no vacancies under staffing plan {0},Il n'y a pas de postes vacants dans le plan de dotation en personnel {0}, +There are no vacancies under staffing plan {0},Il n'y a pas de postes vacants dans le plan de dotation en personnel {0}, This Service Level Agreement is specific to Customer {0},Cet accord de niveau de service est spécifique au client {0}., This action will unlink this account from any external service integrating ERPNext with your bank accounts. It cannot be undone. Are you certain ?,Cette action dissociera ce compte de tout service externe intégrant ERPNext avec vos comptes bancaires. Ça ne peut pas être défait. Êtes-vous sûr ?, This bank account is already synchronized,Ce compte bancaire est déjà synchronisé, @@ -4219,7 +4219,7 @@ Title,Titre, To date needs to be before from date,À ce jour doit être avant la date du, Total,Total, Total Early Exits,Total des sorties anticipées, -Total Late Entries,Nombre total d'entrées en retard, +Total Late Entries,Nombre total d'entrées en retard, Total Payment Request amount cannot be greater than {0} amount,Le montant total de la demande de paiement ne peut être supérieur à {0}., Total payments amount can't be greater than {},Le montant total des paiements ne peut être supérieur à {}, Totals,Totaux, @@ -4230,8 +4230,8 @@ Transport Receipt No and Date are mandatory for your chosen Mode of Transport,Le Tuesday,Mardi, Type,Type, Unable to find Salary Component {0},Impossible de trouver la composante salaire {0}, -Unable to find the time slot in the next {0} days for the operation {1}.,Impossible de trouver l'intervalle de temps dans les {0} jours suivants pour l'opération {1}., -Unable to update remote activity,Impossible de mettre à jour l'activité à distance, +Unable to find the time slot in the next {0} days for the operation {1}.,Impossible de trouver l'intervalle de temps dans les {0} jours suivants pour l'opération {1}., +Unable to update remote activity,Impossible de mettre à jour l'activité à distance, Unknown Caller,Appelant inconnu, Unlink external integrations,Dissocier les intégrations externes, Unmarked Attendance for days,Présence non marquée pendant des jours, @@ -4241,7 +4241,7 @@ Unsupported GST Category for E-Way Bill JSON generation,Catégorie GST non prise Update,Mettre à Jour, Update Details,Détails de mise à jour, Update Taxes for Items,Mettre à jour les taxes pour les articles, -"Upload a bank statement, link or reconcile a bank account","Télécharger un relevé bancaire, un lien ou un rapprochement d'un compte bancaire", +"Upload a bank statement, link or reconcile a bank account","Télécharger un relevé bancaire, un lien ou un rapprochement d'un compte bancaire", Upload a statement,Télécharger une déclaration, Use a name that is different from previous project name,Utilisez un nom différent du nom du projet précédent, User {0} is disabled,Utilisateur {0} est désactivé, @@ -4249,7 +4249,7 @@ Users and Permissions,Utilisateurs et autorisations, Vacancies cannot be lower than the current openings,Les postes vacants ne peuvent pas être inférieurs aux ouvertures actuelles, Valid From Time must be lesser than Valid Upto Time.,La période de validité doit être inférieure à la durée de validité., Valuation Rate required for Item {0} at row {1},Taux de valorisation requis pour le poste {0} à la ligne {1}, -"Valuation rate not found for the Item {0}, which is required to do accounting entries for {1} {2}. If the item is transacting as a zero valuation rate item in the {1}, please mention that in the {1} Item table. Otherwise, please create an incoming stock transaction for the item or mention valuation rate in the Item record, and then try submiting / cancelling this entry.","Taux de valorisation non trouvé pour le poste {0}, nécessaire pour effectuer les écritures comptables pour {1} {2}. Si l'élément effectue une transaction en tant qu'élément à taux d'évaluation zéro dans {1}, veuillez l'indiquer dans le tableau {1} Article. Sinon, créez une transaction de stock entrante pour l'article ou indiquez le taux de valorisation dans l'enregistrement de l'article, puis essayez de soumettre / annuler cette entrée.", +"Valuation rate not found for the Item {0}, which is required to do accounting entries for {1} {2}. If the item is transacting as a zero valuation rate item in the {1}, please mention that in the {1} Item table. Otherwise, please create an incoming stock transaction for the item or mention valuation rate in the Item record, and then try submiting / cancelling this entry.","Taux de valorisation non trouvé pour le poste {0}, nécessaire pour effectuer les écritures comptables pour {1} {2}. Si l'élément effectue une transaction en tant qu'élément à taux d'évaluation zéro dans {1}, veuillez l'indiquer dans le tableau {1} Article. Sinon, créez une transaction de stock entrante pour l'article ou indiquez le taux de valorisation dans l'enregistrement de l'article, puis essayez de soumettre / annuler cette entrée.", Values Out Of Sync,Valeurs désynchronisées, Vehicle Type is required if Mode of Transport is Road,Le type de véhicule est requis si le mode de transport est la route, Vendor Name,Nom du vendeur, @@ -4261,21 +4261,21 @@ Warehouse,Entrepôt, Warehouse not found against the account {0},Entrepôt introuvable sur le compte {0}, Welcome to {0},Bienvenue sur {0}, Why do think this Item should be removed?,Pourquoi pensez-vous que cet élément devrait être supprimé?, -Work Order {0}: Job Card not found for the operation {1},Bon de travail {0}: carte de travail non trouvée pour l'opération {1}, +Work Order {0}: Job Card not found for the operation {1},Bon de travail {0}: carte de travail non trouvée pour l'opération {1}, Workday {0} has been repeated.,La journée de travail {0} a été répétée., XML Files Processed,Fichiers XML traités, Year,Année, Yearly,Annuel, You,Vous, -You are not allowed to enroll for this course,Vous n'êtes pas autorisé à vous inscrire à ce cours, -You are not enrolled in program {0},Vous n'êtes pas inscrit au programme {0}, -You can Feature upto 8 items.,Vous pouvez présenter jusqu'à 8 éléments., +You are not allowed to enroll for this course,Vous n'êtes pas autorisé à vous inscrire à ce cours, +You are not enrolled in program {0},Vous n'êtes pas inscrit au programme {0}, +You can Feature upto 8 items.,Vous pouvez présenter jusqu'à 8 éléments., You can also copy-paste this link in your browser,Vous pouvez également copier-coller ce lien dans votre navigateur, -You can publish upto 200 items.,Vous pouvez publier jusqu'à 200 articles., -You can't create accounting entries in the closed accounting period {0},Vous ne pouvez pas créer d'écritures comptables dans la période comptable clôturée {0}., +You can publish upto 200 items.,Vous pouvez publier jusqu'à 200 articles., +You can't create accounting entries in the closed accounting period {0},Vous ne pouvez pas créer d'écritures comptables dans la période comptable clôturée {0}., You have to enable auto re-order in Stock Settings to maintain re-order levels.,Vous devez activer la re-commande automatique dans les paramètres de stock pour maintenir les niveaux de ré-commande., You must be a registered supplier to generate e-Way Bill,Vous devez être un fournisseur enregistré pour générer une facture électronique, -You need to login as a Marketplace User before you can add any reviews.,Vous devez vous connecter en tant qu'utilisateur de la Marketplace avant de pouvoir ajouter des critiques., +You need to login as a Marketplace User before you can add any reviews.,Vous devez vous connecter en tant qu'utilisateur de la Marketplace avant de pouvoir ajouter des critiques., Your Featured Items,Vos articles en vedette, Your Items,Vos articles, Your Profile,Votre profil, @@ -4291,9 +4291,9 @@ woocommerce - {0},woocommerce - {0}, {0} bank transaction(s) created and {1} errors,{0} transaction (s) bancaire (s) créée (s) et {1} erreur (s), {0} can not be greater than {1},{0} ne peut pas être supérieur à {1}, {0} conversations,{0} conversations, -{0} is not a company bank account,{0} n'est pas un compte bancaire d'entreprise, -{0} is not a group node. Please select a group node as parent cost center,{0} n'est pas un nœud de groupe. Veuillez sélectionner un nœud de groupe comme centre de coûts parent, -{0} is not the default supplier for any items.,{0} n'est le fournisseur par défaut d'aucun élément., +{0} is not a company bank account,{0} n'est pas un compte bancaire d'entreprise, +{0} is not a group node. Please select a group node as parent cost center,{0} n'est pas un nœud de groupe. Veuillez sélectionner un nœud de groupe comme centre de coûts parent, +{0} is not the default supplier for any items.,{0} n'est le fournisseur par défaut d'aucun élément., {0} is required,{0} est nécessaire, {0} units of {1} is not available.,{0} unités de {1} ne sont pas disponibles., {0}: {1} must be less than {2},{0}: {1} doit être inférieur à {2}, @@ -4329,7 +4329,7 @@ Loan Amount is mandatory,Le montant du prêt est obligatoire, Mode Of Payment,Mode de Paiement, No students Found,Aucun étudiant trouvé, Not in Stock,En Rupture de Stock, -Please select a Customer,S'il vous plaît sélectionner un client, +Please select a Customer,S'il vous plaît sélectionner un client, Printed On,Imprimé sur, Received From,Reçu de, Sales Person,Vendeur, @@ -4339,7 +4339,7 @@ Write Off,Reprise, Email Id,Identifiant Email, No,Non, Reference Doctype,DocType de la Référence, -User Id,Identifiant d'utilisateur, +User Id,Identifiant d'utilisateur, Yes,Oui, Actual ,Réel, Add to cart,Ajouter au Panier, @@ -4370,7 +4370,7 @@ Open Projects ,Projets ouverts, Open To Do ,ToDo ouvertes, Operation Id,ID de l'Opération, Partially ordered,Partiellement Ordonné, -Please select company first,Sélectionnez d'abord l'entreprise, +Please select company first,Sélectionnez d'abord l'entreprise, Please select patient,Veuillez sélectionner un patient, Printed On ,Imprimé sur, Projected qty,Qté Projetée, @@ -4378,7 +4378,7 @@ Sales person,Vendeur, Serial No {0} Created,N° de Série {0} créé, Set as default,Définir par défaut, Source Location is required for the Asset {0},La localisation source est requis pour l'actif {0}, -Tax Id,Numéro d'identification fiscale, +Tax Id,Numéro d'identification fiscale, To Time,Horaire de Fin, To date cannot be before from date,À ce jour ne peut pas être antérieure à la date du, Total Taxable value,Valeur taxable totale, @@ -4394,19 +4394,19 @@ to,à, Cards,Cartes, Percentage,Pourcentage, Failed to setup defaults for country {0}. Please contact support@erpnext.com,Échec de la configuration des paramètres par défaut pour le pays {0}. Veuillez contacter support@erpnext.com, -Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.,Ligne # {0}: l'article {1} n'est pas un article sérialisé / en lot. Il ne peut pas avoir de numéro de série / de lot contre lui., +Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.,Ligne # {0}: l'article {1} n'est pas un article sérialisé / en lot. Il ne peut pas avoir de numéro de série / de lot contre lui., Please set {0},Veuillez définir {0}, Please set {0},Veuillez définir {0},supplier Draft,Brouillon,"docstatus,=,0" Cancelled,Annulé,"docstatus,=,2" -Please setup Instructor Naming System in Education > Education Settings,Veuillez configurer le système de dénomination de l'instructeur dans Éducation> Paramètres de l'éducation, +Please setup Instructor Naming System in Education > Education Settings,Veuillez configurer le système de dénomination de l'instructeur dans Éducation> Paramètres de l'éducation, Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir la série de noms pour {0} via Configuration> Paramètres> Série de noms, -UOM Conversion factor ({0} -> {1}) not found for item: {2},Facteur de conversion UdM ({0} -> {1}) introuvable pour l'article: {2}, -Item Code > Item Group > Brand,Code article> Groupe d'articles> Marque, +UOM Conversion factor ({0} -> {1}) not found for item: {2},Facteur de conversion UdM ({0} -> {1}) introuvable pour l'article: {2}, +Item Code > Item Group > Brand,Code article> Groupe d'articles> Marque, Customer > Customer Group > Territory,Client> Groupe de clients> Territoire, Supplier > Supplier Type,Fournisseur> Type de fournisseur, Please setup Employee Naming System in Human Resource > HR Settings,Veuillez configurer le système de dénomination des employés dans Ressources humaines> Paramètres RH, -Please setup numbering series for Attendance via Setup > Numbering Series,Veuillez configurer la série de numérotation pour l'assistance via Configuration> Série de numérotation, +Please setup numbering series for Attendance via Setup > Numbering Series,Veuillez configurer la série de numérotation pour l'assistance via Configuration> Série de numérotation, Purchase Order Required,Bon de Commande Requis, Purchase Receipt Required,Reçu d’Achat Requis, Requested,Demandé, @@ -4462,7 +4462,7 @@ Accounts Frozen Upto,Comptes Gelés Jusqu'au, "Accounting entry frozen up to this date, nobody can do / modify entry except role specified below.","Les écritures comptables sont gelées jusqu'à cette date, personne ne peut ajouter / modifier les entrées sauf les rôles spécifiés ci-dessous.", Role Allowed to Set Frozen Accounts & Edit Frozen Entries,Rôle Autorisé à Geler des Comptes & à Éditer des Écritures Gelées, Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts,Les utilisateurs ayant ce rôle sont autorisés à définir les comptes gelés et à créer / modifier des écritures comptables sur des comptes gelés, -Determine Address Tax Category From,Déterminer la catégorie de taxe d'adresse à partir de, +Determine Address Tax Category From,Déterminer la catégorie de taxe d'adresse à partir de, Address used to determine Tax Category in transactions.,Adresse utilisée pour déterminer la catégorie de taxe dans les transactions., Over Billing Allowance (%),Frais de facturation excédentaires (%), Percentage you are allowed to bill more against the amount ordered. For example: If the order value is $100 for an item and tolerance is set as 10% then you are allowed to bill for $110.,"Pourcentage vous êtes autorisé à facturer plus par rapport au montant commandé. Par exemple: Si la valeur de la commande est de 100 USD pour un article et que la tolérance est définie sur 10%, vous êtes autorisé à facturer 110 USD.", @@ -4471,14 +4471,14 @@ Role that is allowed to submit transactions that exceed credit limits set.,Rôle Check Supplier Invoice Number Uniqueness,Vérifiez l'Unicité du Numéro de Facture du Fournisseur, Make Payment via Journal Entry,Effectuer un Paiement par une Écriture de Journal, Unlink Payment on Cancellation of Invoice,Délier Paiement à l'Annulation de la Facture, -Unlink Advance Payment on Cancelation of Order,Dissocier le paiement anticipé lors de l'annulation d'une commande, +Unlink Advance Payment on Cancelation of Order,Dissocier le paiement anticipé lors de l'annulation d'une commande, Book Asset Depreciation Entry Automatically,Comptabiliser les Entrées de Dépréciation d'Actifs Automatiquement, Allow Cost Center In Entry of Balance Sheet Account,Autoriser le centre de coûts en saisie du compte de bilan, Automatically Add Taxes and Charges from Item Tax Template,Ajouter automatiquement des taxes et des frais à partir du modèle de taxe à la pièce, Automatically Fetch Payment Terms,Récupérer automatiquement les conditions de paiement, Show Inclusive Tax In Print,Afficher la taxe inclusive en impression, Show Payment Schedule in Print,Afficher le calendrier de paiement dans Imprimer, -Currency Exchange Settings,Paramètres d'échange de devises, +Currency Exchange Settings,Paramètres d'échange de devises, Allow Stale Exchange Rates,Autoriser les Taux de Change Existants, Stale Days,Journées Passées, Report Settings,Paramètres de rapport, @@ -4489,20 +4489,20 @@ Branch Code,Code de la branche, Address and Contact,Adresse et Contact, Address HTML,Adresse HTML, Contact HTML,HTML du Contact, -Data Import Configuration,Configuration de l'importation de données, +Data Import Configuration,Configuration de l'importation de données, Bank Transaction Mapping,Cartographie des transactions bancaires, -Plaid Access Token,Jeton d'accès plaid, -Company Account,Compte d'entreprise, +Plaid Access Token,Jeton d'accès plaid, +Company Account,Compte d'entreprise, Account Subtype,Sous-type de compte, Is Default Account,Est un compte par défaut, -Is Company Account,Est le compte de l'entreprise, +Is Company Account,Est le compte de l'entreprise, Party Details,Parti Détails, Account Details,Détails du compte, IBAN,IBAN, Bank Account No,No de compte bancaire, -Integration Details,Détails d'intégration, -Integration ID,ID d'intégration, -Last Integration Date,Dernière date d'intégration, +Integration Details,Détails d'intégration, +Integration ID,ID d'intégration, +Last Integration Date,Dernière date d'intégration, Change this date manually to setup the next synchronization start date,Modifiez cette date manuellement pour définir la prochaine date de début de la synchronisation., Mask,Masque, Bank Guarantee,Garantie Bancaire, @@ -4609,7 +4609,7 @@ POS-CLO-,POS-CLO-, Custody,Garde, Net Amount,Montant Net, Cashier Closing Payments,Paiements de clôture du caissier, -Import Chart of Accounts from a csv file,Importer un tableau de comptes à partir d'un fichier csv, +Import Chart of Accounts from a csv file,Importer un tableau de comptes à partir d'un fichier csv, Attach custom Chart of Accounts file,Joindre un fichier de plan comptable personnalisé, Chart Preview,Aperçu du graphique, Chart Tree,Arbre à cartes, @@ -4641,7 +4641,7 @@ lft,Lft, rgt,rgt, Coupon Code,Code de coupon, Coupon Name,Nom du coupon, -"e.g. ""Summer Holiday 2019 Offer 20""",ex. "Offre vacances d'été 2019 20", +"e.g. ""Summer Holiday 2019 Offer 20""",ex. "Offre vacances d'été 2019 20", Coupon Type,Type de coupon, Promotional,Promotionnel, Gift Card,Carte cadeau, @@ -4692,9 +4692,9 @@ Bank Charges Account,Compte de frais bancaires, Accounts Receivable Credit Account,Compte de crédit débiteur, Accounts Receivable Discounted Account,Compte escompté des comptes débiteurs, Accounts Receivable Unpaid Account,Comptes débiteurs non payés, -Item Tax Template,Modèle de taxe d'article, -Tax Rates,Les taux d'imposition, -Item Tax Template Detail,Détail du modèle de taxe d'article, +Item Tax Template,Modèle de taxe d'article, +Tax Rates,Les taux d'imposition, +Item Tax Template Detail,Détail du modèle de taxe d'article, Entry Type,Type d'Écriture, Inter Company Journal Entry,Ecriture de journal inter-sociétés, Bank Entry,Écriture Bancaire, @@ -4730,7 +4730,7 @@ Debit in Company Currency,Débit en Devise Société, Credit in Company Currency,Crédit dans la Devise de la Société, Payroll Entry,Entrée de la paie, Employee Advance,Avance versée aux employés, -Reference Due Date,Date d'échéance de référence, +Reference Due Date,Date d'échéance de référence, Loyalty Program Tier,Echelon de programme de fidélité, Redeem Against,Échanger contre, Expiry Date,Date d'expiration, @@ -4748,8 +4748,8 @@ Collection Rules,Règles de collecte, Redemption,Echange, Conversion Factor,Facteur de Conversion, 1 Loyalty Points = How much base currency?,1 point de fidélité = Quel montant en devise de base ?, -Expiry Duration (in days),Durée d'expiration (en jours), -Help Section,Section d'aide, +Expiry Duration (in days),Durée d'expiration (en jours), +Help Section,Section d'aide, Loyalty Program Help,Aide au programme de fidélité, Loyalty Program Collection,Collecte du programme de fidélité, Tier Name,Nom de l'échelon, @@ -4767,17 +4767,17 @@ Monthly Distribution Percentage,Pourcentage de Répartition Mensuelle, Percentage Allocation,Allocation en Pourcentage, Create Missing Party,Créer les tiers manquants, Create missing customer or supplier.,Créer les clients ou les fournisseurs manquant, -Opening Invoice Creation Tool Item,Ouverture d'un outil de création de facture, +Opening Invoice Creation Tool Item,Ouverture d'un outil de création de facture, Temporary Opening Account,Compte temporaire d'ouverture, Party Account,Compte de Tiers, Type of Payment,Type de Paiement, ACC-PAY-.YYYY.-,ACC-PAY-YYYY.-, Receive,Recevoir, Internal Transfer,Transfert Interne, -Payment Order Status,État de l'ordre de paiement, +Payment Order Status,État de l'ordre de paiement, Payment Ordered,Paiement commandé, Payment From / To,Paiement De / À, -Company Bank Account,Compte bancaire de l'entreprise, +Company Bank Account,Compte bancaire de l'entreprise, Party Bank Account,Compte bancaire du parti, Account Paid From,Compte Payé Du, Account Paid To,Compte Payé Au, @@ -4802,8 +4802,8 @@ Payment Gateway Account,Compte Passerelle de Paiement, Payment Account,Compte de Paiement, Default Payment Request Message,Message de Demande de Paiement par Défaut, PMO-,PMO-, -Payment Order Type,Type d'ordre de paiement, -Payment Order Reference,Référence de l'ordre de paiement, +Payment Order Type,Type d'ordre de paiement, +Payment Order Reference,Référence de l'ordre de paiement, Bank Account Details,Détails de compte en banque, Payment Reconciliation,Réconciliation des Paiements, Receivable / Payable Account,Compte Débiteur / Créditeur, @@ -4822,14 +4822,14 @@ Payment Reconciliation Payment,Paiement de Réconciliation des Paiements, Reference Row,Ligne de Référence, Allocated amount,Montant alloué, Payment Request Type,Type de demande de paiement, -Outward,À l'extérieur, -Inward,Vers l'intérieur, +Outward,À l'extérieur, +Inward,Vers l'intérieur, ACC-PRQ-.YYYY.-,ACC-PRQ-.AAAA.-, Transaction Details,détails de la transaction, Amount in customer's currency,Montant dans la devise du client, Is a Subscription,Est un abonnement, Transaction Currency,Devise de la Transaction, -Subscription Plans,Plans d'abonnement, +Subscription Plans,Plans d'abonnement, SWIFT Number,Numéro SWIFT, Recipient Message And Payment Details,Message du Destinataire et Détails de Paiement, Make Sales Invoice,Faire des Factures de Vente, @@ -4840,7 +4840,7 @@ Payment Schedule,Calendrier de paiement, Invoice Portion,Pourcentage de facturation, Payment Amount,Montant du paiement, Payment Term Name,Nom du terme de paiement, -Due Date Based On,Date d'échéance basée sur, +Due Date Based On,Date d'échéance basée sur, Day(s) after invoice date,Jour (s) après la date de la facture, Day(s) after the end of the invoice month,Jour (s) après la fin du mois de facture, Month(s) after the end of the invoice month,Mois (s) après la fin du mois de la facture, @@ -4859,12 +4859,12 @@ Update Stock,Mettre à Jour le Stock, Ignore Pricing Rule,Ignorez Règle de Prix, Allow user to edit Rate,Autoriser l'utilisateur à modifier le Taux, Allow user to edit Discount,Autoriser l'utilisateur à modifier la remise, -Allow Print Before Pay,Autoriser l'impression avant la paie, +Allow Print Before Pay,Autoriser l'impression avant la paie, Display Items In Stock,Afficher les articles en stock, Applicable for Users,Applicable aux Utilisateurs, Sales Invoice Payment,Paiement de la Facture de Vente, -Item Groups,Groupes d'articles, -Only show Items from these Item Groups,Afficher uniquement les éléments de ces groupes d'éléments, +Item Groups,Groupes d'articles, +Only show Items from these Item Groups,Afficher uniquement les éléments de ces groupes d'éléments, Customer Groups,Groupes de Clients, Only show Customer of these Customer Groups,Afficher uniquement les clients de ces groupes de clients, Print Format for Online,Format d'Impression en Ligne, @@ -4878,8 +4878,8 @@ POS Profile User,Utilisateur du profil PDV, Use POS in Offline Mode,Utiliser PDV en Mode Hors-Ligne, Apply On,Appliquer Sur, Price or Product Discount,Prix ou remise de produit, -Apply Rule On Item Code,Appliquer la règle sur le code d'article, -Apply Rule On Item Group,Appliquer une règle sur un groupe d'articles, +Apply Rule On Item Code,Appliquer la règle sur le code d'article, +Apply Rule On Item Group,Appliquer une règle sur un groupe d'articles, Apply Rule On Brand,Appliquer la règle sur la marque, Mixed Conditions,Conditions mixtes, Conditions will be applied on all the selected items combined. ,Des conditions seront appliquées sur tous les éléments sélectionnés combinés., @@ -4906,7 +4906,7 @@ Product Discount Scheme,Schéma de remise de produit, Same Item,Même article, Free Item,Article gratuit, Threshold for Suggestion,Seuil de suggestion, -System will notify to increase or decrease quantity or amount ,Le système notifiera d'augmenter ou de diminuer la quantité ou le montant, +System will notify to increase or decrease quantity or amount ,Le système notifiera d'augmenter ou de diminuer la quantité ou le montant, "Higher the number, higher the priority","Plus le nombre est grand, plus la priorité est haute", Apply Multiple Pricing Rules,Appliquer plusieurs règles de tarification, Apply Discount on Rate,Appliquer une réduction sur le taux, @@ -4917,7 +4917,7 @@ Promotional Scheme Id,Id de schéma promotionnel, Promotional Scheme,Schéma promotionnel, Pricing Rule Brand,Marque de règle de tarification, Pricing Rule Detail,Détail de la règle de tarification, -Child Docname,Nom de l'enfant, +Child Docname,Nom de l'enfant, Rule Applied,Règle appliquée, Pricing Rule Item Code,Règle de tarification Article Code, Pricing Rule Item Group,Groupe de postes de règle de tarification, @@ -4933,7 +4933,7 @@ Tax Withholding Category,Catégorie de taxation à la source, Edit Posting Date and Time,Modifier la Date et l'Heure de la Publication, Is Paid,Est Payé, Is Return (Debit Note),Est une note de débit, -Apply Tax Withholding Amount,Appliquer le montant de la retenue d'impôt, +Apply Tax Withholding Amount,Appliquer le montant de la retenue d'impôt, Accounting Dimensions ,Dimensions comptables, Supplier Invoice Details,Détails de la Facture du Fournisseur, Supplier Invoice Date,Date de la Facture du Fournisseur, @@ -5021,7 +5021,7 @@ Is Fixed Asset,Est Immobilisation, Asset Location,Localisation de l'actif, Deferred Expense,Frais différés, Deferred Expense Account,Compte de dépenses différées, -Service Stop Date,Date d'arrêt du service, +Service Stop Date,Date d'arrêt du service, Enable Deferred Expense,Activer les frais reportés, Service Start Date,Date de début du service, Service End Date,Date de fin du service, @@ -5029,8 +5029,8 @@ Allow Zero Valuation Rate,Autoriser un Taux de Valorisation Égal à Zéro, Item Tax Rate,Taux de la Taxe sur l'Article, Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges,La table de détails de taxe est récupérée depuis les données de base de l'article comme une chaîne de caractères et stockée dans ce champ. Elle est utilisée pour les Taxes et Frais., Purchase Order Item,Article du Bon de Commande, -Purchase Receipt Detail,Détail du reçu d'achat, -Item Weight Details,Détails du poids de l'article, +Purchase Receipt Detail,Détail du reçu d'achat, +Item Weight Details,Détails du poids de l'article, Weight Per Unit,Poids par unité, Total Weight,Poids total, Weight UOM,UDM de Poids, @@ -5042,7 +5042,7 @@ Add or Deduct,Ajouter ou Déduire, Deduct,Déduire, On Previous Row Amount,Le Montant de la Rangée Précédente, On Previous Row Total,Le Total de la Rangée Précédente, -On Item Quantity,Sur quantité d'article, +On Item Quantity,Sur quantité d'article, Reference Row #,Ligne de Référence #, Is this Tax included in Basic Rate?,Cette Taxe est-elle incluse dans le Taux de Base ?, "If checked, the tax amount will be considered as already included in the Print Rate / Print Amount","Si cochée, le montant de la taxe sera considéré comme déjà inclus dans le Taux d'Impression / Prix d'Impression", @@ -5104,7 +5104,7 @@ Qty as per Stock UOM,Qté par UDM du Stock, Discount and Margin,Remise et Marge, Rate With Margin,Tarif Avec Marge, Discount (%) on Price List Rate with Margin,Remise (%) sur le Tarif de la Liste de Prix avec la Marge, -Rate With Margin (Company Currency),Taux avec marge (devise de l'entreprise), +Rate With Margin (Company Currency),Taux avec marge (devise de l'entreprise), Delivered By Supplier,Livré par le Fournisseur, Deferred Revenue,Produits comptabilisés d'avance, Deferred Revenue Account,Compte de produits comptabilisés d'avance, @@ -5134,7 +5134,7 @@ From Folio No,Du No de Folio, To Shareholder,A l'actionnaire, To Folio No,Au N. de Folio, Equity/Liability Account,Compte de capitaux propres / passif, -Asset Account,Compte d'actif, +Asset Account,Compte d'actif, (including),(compris), ACC-SH-.YYYY.-,ACC-SH-.YYYY.-, Folio no.,No. de Folio, @@ -5143,7 +5143,7 @@ Hidden list maintaining the list of contacts linked to Shareholder,Liste cachée Specify conditions to calculate shipping amount,Spécifier les conditions pour calculer le montant de la livraison, Shipping Rule Label,Étiquette de la Règle de Livraison, example: Next Day Shipping,Exemple : Livraison le Jour Suivant, -Shipping Rule Type,Type de règle d'expédition, +Shipping Rule Type,Type de règle d'expédition, Shipping Account,Compte de Livraison, Calculate Based On,Calculer en fonction de, Fixed,Fixé, @@ -5157,11 +5157,11 @@ A condition for a Shipping Rule,Une condition pour une Règle de Livraison, From Value,De la Valeur, To Value,Valeur Finale, Shipping Rule Country,Pays de la Règle de Livraison, -Subscription Period,Période d'abonnement, -Subscription Start Date,Date de début de l'abonnement, -Cancelation Date,Date d'annulation, -Trial Period Start Date,Date de début de la période d'essai, -Trial Period End Date,Date de fin de la période d'évaluation, +Subscription Period,Période d'abonnement, +Subscription Start Date,Date de début de l'abonnement, +Cancelation Date,Date d'annulation, +Trial Period Start Date,Date de début de la période d'essai, +Trial Period End Date,Date de fin de la période d'évaluation, Current Invoice Start Date,Date de début de la facture en cours, Current Invoice End Date,Date de fin de la facture en cours, Days Until Due,Jours avant échéance, @@ -5172,17 +5172,17 @@ Plans,Plans, Discounts,Réductions, Additional DIscount Percentage,Pourcentage de réduction supplémentaire, Additional DIscount Amount,Montant de la Remise Supplémentaire, -Subscription Invoice,Facture d'abonnement, -Subscription Plan,Plan d'abonnement, +Subscription Invoice,Facture d'abonnement, +Subscription Plan,Plan d'abonnement, Price Determination,Détermination du prix, Fixed rate,Taux fixe, Based on price list,Sur la base de la liste de prix, Cost,Coût, Billing Interval,Intervalle de facturation, -Billing Interval Count,Nombre d'intervalles de facturation, -"Number of intervals for the interval field e.g if Interval is 'Days' and Billing Interval Count is 3, invoices will be generated every 3 days","Nombre d'intervalles pour le champ d'intervalle, par exemple si Intervalle est "Jours" et si le décompte d'intervalle de facturation est 3, les factures seront générées tous les 3 jours", +Billing Interval Count,Nombre d'intervalles de facturation, +"Number of intervals for the interval field e.g if Interval is 'Days' and Billing Interval Count is 3, invoices will be generated every 3 days","Nombre d'intervalles pour le champ d'intervalle, par exemple si Intervalle est "Jours" et si le décompte d'intervalle de facturation est 3, les factures seront générées tous les 3 jours", Payment Plan,Plan de paiement, -Subscription Plan Detail,Détail du plan d'abonnement, +Subscription Plan Detail,Détail du plan d'abonnement, Plan,Plan, Subscription Settings,Paramètres des Abonnements, Grace Period,Période de grâce, @@ -5200,25 +5200,25 @@ Billing Country,Pays de Facturation, Shipping City,Ville de Livraison, Shipping County,Comté de Livraison, Shipping State,État de livraison, -Shipping Zipcode,Code postal d'expédition, +Shipping Zipcode,Code postal d'expédition, Shipping Country,Pays de Livraison, Tax Withholding Account,Compte de taxation à la source, -Tax Withholding Rates,Taux de retenue d'impôt, +Tax Withholding Rates,Taux de retenue d'impôt, Rates,Prix, -Tax Withholding Rate,Taux de retenue d'impôt, +Tax Withholding Rate,Taux de retenue d'impôt, Single Transaction Threshold,Seuil de transaction unique, Cumulative Transaction Threshold,Seuil de transaction cumulatif, -Agriculture Analysis Criteria,Critères d'analyse de l'agriculture, +Agriculture Analysis Criteria,Critères d'analyse de l'agriculture, Linked Doctype,Doctype lié, -Water Analysis,Analyse de l'eau, +Water Analysis,Analyse de l'eau, Soil Analysis,Analyse du sol, Plant Analysis,Analyse des plantes, Fertilizer,Engrais, Soil Texture,Texture du sol, Weather,Météo, -Agriculture Manager,Directeur de l'agriculture, +Agriculture Manager,Directeur de l'agriculture, Agriculture User,Agriculteur, -Agriculture Task,Tâche d'agriculture, +Agriculture Task,Tâche d'agriculture, Start Day,Date de début, End Day,Jour de fin, Holiday Management,Gestion des vacances, @@ -5231,9 +5231,9 @@ Crop Name,Nom de la culture, Scientific Name,Nom scientifique, "You can define all the tasks which need to carried out for this crop here. The day field is used to mention the day on which the task needs to be carried out, 1 being the 1st day, etc.. ","Vous pouvez définir ici toutes les tâches à effectuer pour cette culture. Le champ de jour est utilisé pour mentionner le jour où la tâche doit être effectuée, 1 étant le 1er jour, etc.", Crop Spacing,Espacement des cultures, -Crop Spacing UOM,UOM d'espacement des cultures, +Crop Spacing UOM,UOM d'espacement des cultures, Row Spacing,Écartement des rangs, -Row Spacing UOM,UOM d'espacement des lignes, +Row Spacing UOM,UOM d'espacement des lignes, Perennial,Vivace, Biennial,Biennal, Planting UOM,Unité de mesure de la plantation, @@ -5248,7 +5248,7 @@ A link to all the Locations in which the Crop is growing,Lien vers tous les lieu This will be day 1 of the crop cycle,Ce sera le jour 1 du cycle de la culture, ISO 8601 standard,Norme ISO 8601, Cycle Type,Type de cycle, -Less than a year,Moins d'un an, +Less than a year,Moins d'un an, The minimum length between each plant in the field for optimum growth,La longueur minimale entre chaque plante sur le terrain pour une croissance optimale, The minimum distance between rows of plants for optimum growth,La distance minimale entre les rangées de plantes pour une croissance optimale, Detected Diseases,Maladies détectées, @@ -5260,18 +5260,18 @@ Tasks Created,Tâches créées, Common Name,Nom commun, Treatment Task,Tâche de traitement, Treatment Period,Période de traitement, -Fertilizer Name,Nom de l'engrais, +Fertilizer Name,Nom de l'engrais, Density (if liquid),Densité (si liquide), -Fertilizer Contents,Contenu de l'engrais, -Fertilizer Content,Contenu d'engrais, +Fertilizer Contents,Contenu de l'engrais, +Fertilizer Content,Contenu d'engrais, Linked Plant Analysis,Analyse des plantes liées, Linked Soil Analysis,Analyse de sol liée, Linked Soil Texture,Texture de sol liée, Collection Datetime,Date et heure du prélèvement, Laboratory Testing Datetime,Date et heure du test de laboratoire, Result Datetime,Date et heure du résultat, -Plant Analysis Criterias,Critères d'analyse des plantes, -Plant Analysis Criteria,Critères d'analyse des plantes, +Plant Analysis Criterias,Critères d'analyse des plantes, +Plant Analysis Criteria,Critères d'analyse des plantes, Minimum Permissible Value,Valeur minimale autorisée, Maximum Permissible Value,Valeur maximale autorisée, Ca/K,Ca / K, @@ -5279,31 +5279,31 @@ Ca/Mg,Ca / Mg, Mg/K,Mg / K, (Ca+Mg)/K,(Ca + Mg) / K, Ca/(K+Ca+Mg),Ca / (K + Ca + Mg), -Soil Analysis Criterias,Critères d'analyse des sols, -Soil Analysis Criteria,Critères d'analyse des sols, +Soil Analysis Criterias,Critères d'analyse des sols, +Soil Analysis Criteria,Critères d'analyse des sols, Soil Type,Le type de sol, Loamy Sand,Sable limoneux, Sandy Loam,Limon sableux, Loam,Terreau, Silt Loam,Limon fin, Sandy Clay Loam,Limon argilo-sableux, -Clay Loam,Terreau d'argile, +Clay Loam,Terreau d'argile, Silty Clay Loam,Limon argileux fin, Sandy Clay,Argile sableuse, Silty Clay,Argile limoneuse, -Clay Composition (%),Composition d'argile (%), +Clay Composition (%),Composition d'argile (%), Sand Composition (%),Composition de sable (%), Silt Composition (%),Composition de limon (%), Ternary Plot,Tracé ternaire, Soil Texture Criteria,Critères de texture du sol, -Type of Sample,Type d'échantillon, +Type of Sample,Type d'échantillon, Container,Récipient, Origin,Origine, Collection Temperature ,Température de collecte, Storage Temperature,Température de stockage, Appearance,Apparence, Person Responsible,Personne responsable, -Water Analysis Criteria,Critères d'analyse de l'eau, +Water Analysis Criteria,Critères d'analyse de l'eau, Weather Parameter,Paramètre météo, ACC-ASS-.YYYY.-,ACC-ASS-YYYY.-, Asset Owner,Propriétaire de l'Actif, @@ -5313,7 +5313,7 @@ Disposal Date,Date d’Élimination, Journal Entry for Scrap,Écriture de Journal pour la Mise au Rebut, Available-for-use Date,Date de mise en service, Calculate Depreciation,Calculer la dépréciation, -Allow Monthly Depreciation,Autoriser l'amortissement mensuel, +Allow Monthly Depreciation,Autoriser l'amortissement mensuel, Number of Depreciations Booked,Nombre d’Amortissements Comptabilisés, Finance Books,Livres comptables, Straight Line,Linéaire, @@ -5328,17 +5328,17 @@ Depreciation Schedules,Calendriers d'Amortissement, Policy number,Numéro de politique, Insurer,Assureur, Insured value,Valeur assurée, -Insurance Start Date,Date de début de l'assurance, -Insurance End Date,Date de fin de l'assurance, +Insurance Start Date,Date de début de l'assurance, +Insurance End Date,Date de fin de l'assurance, Comprehensive Insurance,Assurance complète, Maintenance Required,Maintenance requise, -Check if Asset requires Preventive Maintenance or Calibration,Vérifier si l'actif nécessite une maintenance préventive ou un étalonnage, +Check if Asset requires Preventive Maintenance or Calibration,Vérifier si l'actif nécessite une maintenance préventive ou un étalonnage, Booked Fixed Asset,Actif immobilisé comptabilisé, -Purchase Receipt Amount,Montant du reçu d'achat, +Purchase Receipt Amount,Montant du reçu d'achat, Default Finance Book,Livre comptable par défaut, Quality Manager,Responsable Qualité, Asset Category Name,Nom de Catégorie d'Actif, -Depreciation Options,Options d'amortissement, +Depreciation Options,Options d'amortissement, Enable Capital Work in Progress Accounting,Activer la comptabilité des immobilisations en cours, Finance Book Detail,Détails du livre comptable, Asset Category Account,Compte de Catégorie d'Actif, @@ -5348,9 +5348,9 @@ Depreciation Expense Account,Compte de Dotations aux Amortissement, Capital Work In Progress Account,Compte d'immobilisation en cours, Asset Finance Book,Livre comptable d'actifs, Written Down Value,Valeur comptable nette, -Depreciation Start Date,Date de début de l'amortissement, +Depreciation Start Date,Date de début de l'amortissement, Expected Value After Useful Life,Valeur Attendue Après Utilisation Complète, -Rate of Depreciation,Taux d'amortissement, +Rate of Depreciation,Taux d'amortissement, In Percentage,En pourcentage, Select Serial No,Veuillez sélectionner le numéro de série, Maintenance Team,Équipe de maintenance, @@ -5369,43 +5369,43 @@ Preventive Maintenance,Maintenance préventive, Calibration,Étalonnage, 2 Yearly,2 ans, Certificate Required,Certificat requis, -Next Due Date,prochaine date d'échéance, -Last Completion Date,Dernière date d'achèvement, +Next Due Date,prochaine date d'échéance, +Last Completion Date,Dernière date d'achèvement, Asset Maintenance Team,Équipe de Maintenance des Actifs, -Maintenance Team Name,Nom de l'équipe de maintenance, -Maintenance Team Members,Membres de l'équipe de maintenance, +Maintenance Team Name,Nom de l'équipe de maintenance, +Maintenance Team Members,Membres de l'équipe de maintenance, Purpose,Objet, Stock Manager,Responsable des Stocks, -Asset Movement Item,Élément de mouvement d'actif, +Asset Movement Item,Élément de mouvement d'actif, Source Location,Localisation source, From Employee,De l'Employé, Target Location,Localisation cible, -To Employee,À l'employé, +To Employee,À l'employé, Asset Repair,Réparation d'Actif, ACC-ASR-.YYYY.-,ACC-ASR-.AAAA.-, -Failure Date,Date d'échec, +Failure Date,Date d'échec, Assign To Name,Attribuer au nom, Repair Status,État de réparation, Error Description,Erreur de description, -Downtime,Temps d'arrêt, +Downtime,Temps d'arrêt, Repair Cost,Coût de réparation, Manufacturing Manager,Responsable de Production, -Current Asset Value,Valeur actuelle de l'actif, -New Asset Value,Nouvelle valeur de l'actif, +Current Asset Value,Valeur actuelle de l'actif, +New Asset Value,Nouvelle valeur de l'actif, Make Depreciation Entry,Créer une Écriture d'Amortissement, Finance Book Id,Identifiant du livre comptable, Location Name,Nom du lieux, Parent Location,Localisation parente, Is Container,Est le contenant, -Check if it is a hydroponic unit,Vérifiez s'il s'agit d'une unité hydroponique, -Location Details,Détails de l'emplacement, +Check if it is a hydroponic unit,Vérifiez s'il s'agit d'une unité hydroponique, +Location Details,Détails de l'emplacement, Latitude,Latitude, Longitude,Longitude, Area,Région, Area UOM,Unité de mesure de la surface, Tree Details,Détails de l’Arbre, -Maintenance Team Member,Membre de l'équipe de maintenance, -Team Member,Membre de l'équipe, +Maintenance Team Member,Membre de l'équipe de maintenance, +Team Member,Membre de l'équipe, Maintenance Role,Rôle de maintenance, Buying Settings,Paramètres d'Achat, Settings for Buying Module,Paramètres du Module Achat, @@ -5428,7 +5428,7 @@ Customer Contact Email,Email Contact Client, Set Target Warehouse,Définir le magasin cible, Supply Raw Materials,Fournir les Matières Premières, Purchase Order Pricing Rule,Règle de tarification des bons de commande, -Set Reserve Warehouse,Définir l'entrepôt de réserve, +Set Reserve Warehouse,Définir l'entrepôt de réserve, In Words will be visible once you save the Purchase Order.,En Toutes Lettres. Sera visible une fois que vous enregistrerez le Bon de Commande., Advance Paid,Avance Payée, % Billed,% Facturé, @@ -5535,23 +5535,23 @@ Parameter Name,Nom du Paramètre, Supplier Scorecard Standing,Classement de la Fiche d'Évaluation Fournisseur, Notify Other,Notifier Autre, Supplier Scorecard Variable,Variable de la Fiche d'Évaluation Fournisseur, -Call Log,Journal d'appel, +Call Log,Journal d'appel, Received By,Reçu par, -Caller Information,Informations sur l'appelant, +Caller Information,Informations sur l'appelant, Contact Name,Nom du Contact, Lead Name,Nom du Prospect, Ringing,Sonnerie, Missed,Manqué, -Call Duration in seconds,Durée d'appel en secondes, -Recording URL,URL d'enregistrement, +Call Duration in seconds,Durée d'appel en secondes, +Recording URL,URL d'enregistrement, Communication Medium,Moyen de Communication, Communication Medium Type,Type de support de communication, Voice,Voix, Catch All,Attraper tout, -"If there is no assigned timeslot, then communication will be handled by this group","S'il n'y a pas d'intervalle de temps attribué, la communication sera gérée par ce groupe.", +"If there is no assigned timeslot, then communication will be handled by this group","S'il n'y a pas d'intervalle de temps attribué, la communication sera gérée par ce groupe.", Timeslots,Tranches de temps, Communication Medium Timeslot,Période de communication moyenne, -Employee Group,Groupe d'employés, +Employee Group,Groupe d'employés, Appointment,Rendez-Vous, Scheduled Time,Heure prévue, Unverified,Non vérifié, @@ -5563,18 +5563,18 @@ Appointment With,Rendez-vous avec, Calendar Event,Événement de calendrier, Appointment Booking Settings,Paramètres de réservation de rendez-vous, Enable Appointment Scheduling,Activer la planification des rendez-vous, -Agent Details,Détails de l'agent, +Agent Details,Détails de l'agent, Availability Of Slots,Disponibilité des emplacements, Number of Concurrent Appointments,Nombre de rendez-vous simultanés, Agents,Agents, Appointment Details,Détails du rendez-vous, Appointment Duration (In Minutes),Durée du rendez-vous (en minutes), Notify Via Email,Avertir par e-mail, -Notify customer and agent via email on the day of the appointment.,Avertissez le client et l'agent par e-mail le jour du rendez-vous., -Number of days appointments can be booked in advance,Nombre de jours de rendez-vous peuvent être réservés à l'avance, +Notify customer and agent via email on the day of the appointment.,Avertissez le client et l'agent par e-mail le jour du rendez-vous., +Number of days appointments can be booked in advance,Nombre de jours de rendez-vous peuvent être réservés à l'avance, Success Settings,Paramètres de réussite, Success Redirect URL,URL de redirection réussie, -"Leave blank for home.\nThis is relative to site URL, for example ""about"" will redirect to ""https://yoursitename.com/about""","Laissez vide pour la maison. Ceci est relatif à l'URL du site, par exemple "about" redirigera vers "https://yoursitename.com/about"", +"Leave blank for home.\nThis is relative to site URL, for example ""about"" will redirect to ""https://yoursitename.com/about""","Laissez vide pour la maison. Ceci est relatif à l'URL du site, par exemple "about" redirigera vers "https://yoursitename.com/about"", Appointment Booking Slots,Horaires de prise de rendez-vous, From Time ,Horaire de Début, Campaign Email Schedule,Calendrier des e-mails de campagne, @@ -5595,17 +5595,17 @@ Signed On,Signé le, Contract Details,Détails du contrat, Contract Template,Modèle de contrat, Contract Terms,Termes du contrat, -Fulfilment Details,Détails de l'exécution, +Fulfilment Details,Détails de l'exécution, Requires Fulfilment,Nécessite des conditions, -Fulfilment Deadline,Délai d'exécution, -Fulfilment Terms,Conditions d'exécution, -Contract Fulfilment Checklist,Liste de vérification de l'exécution des contrats, +Fulfilment Deadline,Délai d'exécution, +Fulfilment Terms,Conditions d'exécution, +Contract Fulfilment Checklist,Liste de vérification de l'exécution des contrats, Requirement,Obligations, Contract Terms and Conditions,Termes et conditions du contrat, -Fulfilment Terms and Conditions,Termes et conditions d'exécution, +Fulfilment Terms and Conditions,Termes et conditions d'exécution, Contract Template Fulfilment Terms,Conditions d'exécution du modèle de contrat, Email Campaign,Campagne Email, -Email Campaign For ,Campagne d'email pour, +Email Campaign For ,Campagne d'email pour, Lead is an Organization,Le prospect est une organisation, CRM-LEAD-.YYYY.-,CRM-LEAD-.YYYY.-, Person Name,Nom de la Personne, @@ -5668,7 +5668,7 @@ Examiner Name,Nom de l'Examinateur, Supervisor,Superviseur, Supervisor Name,Nom du Superviseur, Evaluate,Évaluer, -Maximum Assessment Score,Score d'évaluation maximale, +Maximum Assessment Score,Score d'évaluation maximale, Assessment Plan Criteria,Critères du Plan d'Évaluation, Maximum Score,Score Maximum, Total Score,Score Total, @@ -5684,10 +5684,10 @@ Course Name,Nom du Cours, Topics,Les sujets, Hero Image,Image de héros, Default Grading Scale,Échelle de Notation par Défault, -Education Manager,Gestionnaire de l'éducation, +Education Manager,Gestionnaire de l'éducation, Course Activity,Activité de cours, Course Enrollment,Inscription au cours, -Activity Date,Date d'activité, +Activity Date,Date d'activité, Course Assessment Criteria,Critères d'Évaluation du Cours, Weightage,Poids, Course Content,Le contenu des cours, @@ -5703,7 +5703,7 @@ Course End Date,Date de Fin du Cours, Course Topic,Sujet du cours, Topic,Sujet, Topic Name,Nom du Sujet, -Education Settings,Paramètres d'éducation, +Education Settings,Paramètres d'éducation, Current Academic Year,Année Académique Actuelle, Current Academic Term,Terme Académique Actuel, Attendance Freeze Date,Date du Gel des Présences, @@ -5712,7 +5712,7 @@ Validate Batch for Students in Student Group,Valider le Lot pour les Étudiants Validate Enrolled Course for Students in Student Group,Valider le Cours Inscrit pour les Étudiants en Groupe Étudiant, "For Course based Student Group, the Course will be validated for every Student from the enrolled Courses in Program Enrollment.","Pour un groupe étudiant basé sur un cours, le cours sera validé pour chaque élève inscrit aux cours du programme.", Make Academic Term Mandatory,Faire un terme académique obligatoire, -"If enabled, field Academic Term will be Mandatory in Program Enrollment Tool.","Si cette option est activée, le champ Période académique sera obligatoire dans l'outil d'inscription au programme.", +"If enabled, field Academic Term will be Mandatory in Program Enrollment Tool.","Si cette option est activée, le champ Période académique sera obligatoire dans l'outil d'inscription au programme.", Instructor Records to be created by,Les Enregistrements de l'Instructeur seront créés par, Employee Number,Numéro d'Employé, LMS Settings,Paramètres LMS, @@ -5760,7 +5760,7 @@ Guardian Interest,Part du Tuteur, Interest,Intérêt, Guardian Student,Tuteur de l'Étudiant, EDU-INS-.YYYY.-,EDU-INS-YYYY.-, -Instructor Log,Journal de l'instructeur, +Instructor Log,Journal de l'instructeur, Other details,Autres Détails, Option,Option, Is Correct,Est correct, @@ -5768,15 +5768,15 @@ Program Name,Nom du Programme, Program Abbreviation,Abréviation du Programme, Courses,Cours, Is Published,Est publié, -Allow Self Enroll,Autoriser l'auto-inscription, +Allow Self Enroll,Autoriser l'auto-inscription, Is Featured,Est en vedette, -Intro Video,Vidéo d'introduction, +Intro Video,Vidéo d'introduction, Program Course,Cours du Programme, School House,Maison de l'École, Boarding Student,Enregistrement Étudiant, Check this if the Student is residing at the Institute's Hostel.,Vérifiez si l'Étudiant réside à la Résidence de l'Institut., Walking,En Marchant, -Institute's Bus,Bus de l'Institut, +Institute's Bus,Bus de l'Institut, Public Transport,Transports Publics, Self-Driving Vehicle,Véhicule Autonome, Pick/Drop by Guardian,Déposé/Récupéré par le Tuteur, @@ -5787,7 +5787,7 @@ Program Enrollment Tool,Outil d’Inscription au Programme, Get Students From,Obtenir les Étudiants De, Student Applicant,Candidature Étudiante, Get Students,Obtenir les Étudiants, -Enrollment Details,Détails d'inscription, +Enrollment Details,Détails d'inscription, New Program,Nouveau Programme, New Student Batch,Nouveau groupe d'étudiants, Enroll Students,Inscrire des Étudiants, @@ -5846,7 +5846,7 @@ Admission Start Date,Date de Début de l'Admission, Admission End Date,Date de Fin de l'Admission, Publish on website,Publier sur le site web, Eligibility and Details,Admissibilité et Détails, -Student Admission Program,Programme d'admission des étudiants, +Student Admission Program,Programme d'admission des étudiants, Minimum Age,Âge Minimum, Maximum Age,Âge Maximum, Application Fee,Frais de Dossier, @@ -5883,14 +5883,14 @@ Will show the student as Present in Student Monthly Attendance Report,Affichera Student Log,Journal des Étudiants, Academic,Académique, Achievement,Réalisation, -Student Report Generation Tool,Outil de génération de rapports d'étudiants, -Include All Assessment Group,Inclure tout le groupe d'évaluation, +Student Report Generation Tool,Outil de génération de rapports d'étudiants, +Include All Assessment Group,Inclure tout le groupe d'évaluation, Show Marks,Afficher les notes, Add letterhead,Ajouter un en-tête, Print Section,Section d'impression, Total Parents Teacher Meeting,Total des réunions parents/professeur, Attended by Parents,Les parents ont participé, -Assessment Terms,Conditions d'évaluation, +Assessment Terms,Conditions d'évaluation, Student Sibling,Frère et Sœur de l'Étudiant, Studying in Same Institute,Étudier au même Institut, Student Siblings,Frères et Sœurs de l'Étudiants, @@ -5898,10 +5898,10 @@ Topic Content,Contenu du sujet, Amazon MWS Settings,Paramètres Amazon MWS, ERPNext Integrations,Intégrations ERPNext, Enable Amazon,Activer Amazon, -MWS Credentials,Informations d'identification MWS, +MWS Credentials,Informations d'identification MWS, Seller ID,ID du vendeur, -AWS Access Key ID,ID de clé d'accès AWS, -MWS Auth Token,Jeton d'authentification MWS, +AWS Access Key ID,ID de clé d'accès AWS, +MWS Auth Token,Jeton d'authentification MWS, Market Place ID,Identifiant de la place du marché, AU,AU, BR,BR, @@ -5919,12 +5919,12 @@ Market Place Account Group,Groupe de comptes de marché, After Date,Après la date, Amazon will synch data updated after this date,Amazon synchronisera les données mises à jour après cette date, Get financial breakup of Taxes and charges data by Amazon ,Obtenez la répartition financière des taxes et des données de facturation par Amazon, -Click this button to pull your Sales Order data from Amazon MWS.,Cliquez sur ce bouton pour extraire vos données de commande client d'Amazon MWS., +Click this button to pull your Sales Order data from Amazon MWS.,Cliquez sur ce bouton pour extraire vos données de commande client d'Amazon MWS., Check this to enable a scheduled Daily synchronization routine via scheduler,Cochez cette case pour activer une routine de synchronisation quotidienne programmée via le planificateur, Max Retry Limit,Max Retry Limit, Exotel Settings,Paramètres Exotel, Account SID,Compte SID, -API Token,Jeton d'API, +API Token,Jeton d'API, GoCardless Mandate,Mandat GoCardless, Mandate,Mandat, GoCardless Customer,Client GoCardless, @@ -5939,15 +5939,15 @@ Plaid Environment,Environnement écossais, sandbox,bac à sable, development,développement, QuickBooks Migrator,QuickBooks Migrator, -Application Settings,Paramètres de l'application, +Application Settings,Paramètres de l'application, Token Endpoint,Point de terminaison de jeton, Scope,Portée, -Authorization Settings,Paramètres d'autorisation, +Authorization Settings,Paramètres d'autorisation, Authorization Endpoint,Autorisation Endpoint, -Authorization URL,URL d'autorisation, +Authorization URL,URL d'autorisation, Quickbooks Company ID,ID Quickbooks de la société, Company Settings,des paramètres de l'entreprise, -Default Shipping Account,Compte d'expédition par défaut, +Default Shipping Account,Compte d'expédition par défaut, Default Warehouse,Entrepôt par Défaut, Default Cost Center,Centre de Coûts par Défaut, Undeposited Funds Account,Compte de fonds non déposés, @@ -5956,7 +5956,7 @@ Request Data,Données de la requête, Shopify Settings,Paramètres de Shopify, status html,Statut, Enable Shopify,Activer Shopify, -App Type,Type d'application, +App Type,Type d'application, Last Sync Datetime,Dernière date de synchronisation, Shop URL,URL de la boutique, eg: frappe.myshopify.com,par exemple: frappe.myshopify.com, @@ -6004,15 +6004,15 @@ Secret,Secret, API consumer key,Clé de consommateur API, API consumer secret,Secret de consommateur API, Tax Account,Compte de taxes, -Freight and Forwarding Account,Compte de fret et d'expédition, +Freight and Forwarding Account,Compte de fret et d'expédition, Creation User,Création utilisateur, "The user that will be used to create Customers, Items and Sales Orders. This user should have the relevant permissions.","Utilisateur qui sera utilisé pour créer des clients, des articles et des commandes clients. Cet utilisateur doit avoir les autorisations appropriées.", -"This warehouse will be used to create Sales Orders. The fallback warehouse is ""Stores"".",Cet entrepôt sera utilisé pour créer des commandes client. L'entrepôt de secours est "Stores"., +"This warehouse will be used to create Sales Orders. The fallback warehouse is ""Stores"".",Cet entrepôt sera utilisé pour créer des commandes client. L'entrepôt de secours est "Stores"., "The fallback series is ""SO-WOO-"".",La série de repli est "SO-WOO-"., This company will be used to create Sales Orders.,Cette société sera utilisée pour créer des commandes client., Delivery After (Days),Livraison après (jours), -This is the default offset (days) for the Delivery Date in Sales Orders. The fallback offset is 7 days from the order placement date.,Il s'agit du décalage par défaut (jours) pour la date de livraison dans les commandes client. La compensation de repli est de 7 jours à compter de la date de passation de la commande., -"This is the default UOM used for items and Sales orders. The fallback UOM is ""Nos"".",Il s'agit de l'UOM par défaut utilisée pour les articles et les commandes clients. La MOU de repli est "Nos"., +This is the default offset (days) for the Delivery Date in Sales Orders. The fallback offset is 7 days from the order placement date.,Il s'agit du décalage par défaut (jours) pour la date de livraison dans les commandes client. La compensation de repli est de 7 jours à compter de la date de passation de la commande., +"This is the default UOM used for items and Sales orders. The fallback UOM is ""Nos"".",Il s'agit de l'UOM par défaut utilisée pour les articles et les commandes clients. La MOU de repli est "Nos"., Endpoints,Points de terminaison, Endpoint,Point de terminaison, Antibiotic Name,Nom de l'Antibiotique, @@ -6056,14 +6056,14 @@ Charges,Charges, Default Currency,Devise par Défaut, Healthcare Schedule Time Slot,Horaire horaire, Parent Service Unit,Service parent, -Service Unit Type,Type d'unité de service, +Service Unit Type,Type d'unité de service, Allow Appointments,Autoriser les rendez-vous, Allow Overlap,Autoriser le chevauchement, Inpatient Occupancy,Occupation des patients hospitalisés, -Occupancy Status,Statut d'occupation, +Occupancy Status,Statut d'occupation, Vacant,Vacant, Occupied,Occupé, -Item Details,Détails d'article, +Item Details,Détails d'article, UOM Conversion in Hours,Conversion UOM en heures, Rate / UOM,Taux / UM, Change in Item,Modification dans l'Article, @@ -6089,7 +6089,7 @@ Reminder Message,Message de Rappel, Remind Before,Rappeler Avant, Laboratory Settings,Paramètres de laboratoire, Employee name and designation in print,Nom et désignation de l'employé sur l'imprimé, -Custom Signature in Print,Signature personnalisée dans l'impression, +Custom Signature in Print,Signature personnalisée dans l'impression, Laboratory SMS Alerts,Alertes SMS de laboratoire, Check In,Arrivée, Check Out,Départ, @@ -6146,7 +6146,7 @@ Require Result Value,Nécessite la Valeur du Résultat, Normal Test Template,Modèle de Test Normal, Patient Demographics,Démographie du Patient, HLC-PAT-.YYYY.-,HLC-PAT-. AAAA.-, -Inpatient Status,Statut d'hospitalisation, +Inpatient Status,Statut d'hospitalisation, Personal and Social History,Antécédents Personnels et Sociaux, Marital Status,État Civil, Married,Marié, @@ -6164,7 +6164,7 @@ Other Risk Factors,Autres facteurs de risque, Patient Details,Détails du patient, Additional information regarding the patient,Informations complémentaires concernant le patient, Patient Age,Âge du patient, -More Info,Plus d'infos, +More Info,Plus d'infos, Referring Practitioner,Praticien référant, Reminded,Rappelé, Parameters,Paramètres, @@ -6181,7 +6181,7 @@ Spouse,Époux, Family,Famille, Schedule Name,Nom du calendrier, Time Slots,Créneaux Horaires, -Practitioner Service Unit Schedule,Horaire de l'unité de service du praticien, +Practitioner Service Unit Schedule,Horaire de l'unité de service du praticien, Procedure Name,Nom de la procédure, Appointment Booked,Rendez-vous pris, Procedure Created,Procédure créée, @@ -6217,25 +6217,25 @@ One Sided,Une face, Blood Pressure (systolic),Pression Artérielle (Systolique), Blood Pressure (diastolic),Pression Artérielle (Diastolique), Blood Pressure,Pression Artérielle, -"Normal resting blood pressure in an adult is approximately 120 mmHg systolic, and 80 mmHg diastolic, abbreviated ""120/80 mmHg""","La tension artérielle normale chez un adulte est d'environ 120 mmHg systolique et 80 mmHg diastolique, abrégé "120/80 mmHg"", +"Normal resting blood pressure in an adult is approximately 120 mmHg systolic, and 80 mmHg diastolic, abbreviated ""120/80 mmHg""","La tension artérielle normale chez un adulte est d'environ 120 mmHg systolique et 80 mmHg diastolique, abrégé "120/80 mmHg"", Nutrition Values,Valeurs Nutritionnelles, Height (In Meter),Hauteur (en Mètres), Weight (In Kilogram),Poids (En Kilogramme), BMI,IMC, -Hotel Room,Chambre d'hôtel, -Hotel Room Type,Type de chambre d'hôtel, +Hotel Room,Chambre d'hôtel, +Hotel Room Type,Type de chambre d'hôtel, Capacity,Capacité, Extra Bed Capacity,Capacité de lits supplémentaire, -Hotel Manager,Directeur de l'hôtel, +Hotel Manager,Directeur de l'hôtel, Hotel Room Amenity,Équipement de la chambre d'hôtel, Billable,Facturable, Hotel Room Package,Forfait de la chambre d'hôtel, Amenities,Équipements, -Hotel Room Pricing,Prix de la chambre d'hôtel, +Hotel Room Pricing,Prix de la chambre d'hôtel, Hotel Room Pricing Item,Article de prix de la chambre d'hôtel, Hotel Room Pricing Package,Forfait de prix de la chambre d'hôtel, Hotel Room Reservation,Réservation de la chambre d'hôtel, -Guest Name,Nom de l'invité, +Guest Name,Nom de l'invité, Late Checkin,Arrivée tardive, Booked,Réservé, Hotel Reservation User,Utilisateur chargé des réservations d'hôtel, @@ -6264,7 +6264,7 @@ Closing Notes,Notes de clôture, Appointment Letter content,Contenu de la lettre de nomination, Appraisal,Estimation, HR-APR-.YY.-.MM.,HR-APR-.YY.-.MM., -Appraisal Template,Modèle d'évaluation, +Appraisal Template,Modèle d'évaluation, For Employee Name,Nom de l'Employé, Goals,Objectifs, Calculate Total Score,Calculer le Résultat Total, @@ -6322,7 +6322,7 @@ Applicable for external driver,Applicable pour pilote externe, Cellphone Number,Numéro de téléphone portable, License Details,Détails de la licence, License Number,Numéro de licence, -Issuing Date,Date d'émission, +Issuing Date,Date d'émission, Driving License Categories,Catégories de permis de conduire, Driving License Category,Catégorie de permis de conduire, Fleet Manager,Gestionnaire de Flotte, @@ -6330,7 +6330,7 @@ Driver licence class,Classe de permis de conduire, HR-EMP-,HR-EMP-, Employment Type,Type d'Emploi, Emergency Contact,Contact en cas d'Urgence, -Emergency Contact Name,Nom à contacter en cas d'urgence, +Emergency Contact Name,Nom à contacter en cas d'urgence, Emergency Phone,Téléphone d'Urgence, ERPNext User,Utilisateur ERPNext, "System User (login) ID. If set, it will become default for all HR forms.","L'ID (de connexion) de l'Utilisateur Système. S'il est défini, il deviendra la valeur par défaut pour tous les formulaires des RH.", @@ -6346,7 +6346,7 @@ Department and Grade,Département et échelon, Reports to,Rapports À, Attendance and Leave Details,Détails de présence et de congés, Leave Policy,Politique de congé, -Attendance Device ID (Biometric/RF tag ID),Périphérique d'assistance (identifiant d'étiquette biométrique / RF), +Attendance Device ID (Biometric/RF tag ID),Périphérique d'assistance (identifiant d'étiquette biométrique / RF), Applicable Holiday List,Liste de Vacances Valable, Default Shift,Décalage par défaut, Salary Details,Détails du salaire, @@ -6397,7 +6397,7 @@ HR-EAD-.YYYY.-,HR-EAD-.YYYY.-, Due Advance Amount,Montant de l'avance dû, Returned Amount,Montant retourné, Claimed,Réclamé, -Advance Account,Compte d'avances, +Advance Account,Compte d'avances, Employee Attendance Tool,Outil de Gestion des Présences des Employés, Unmarked Attendance,Participation Non Marquée, Employees HTML,Employés HTML, @@ -6409,7 +6409,7 @@ Remaining Benefits (Yearly),Prestations sociales restantes (par année), Payroll Period,Période de paie, Benefits Applied,Prestations demandées, Dispensed Amount (Pro-rated),Montant distribué (au prorata), -Employee Benefit Application Detail,Détail de la demande d'avantages sociaux, +Employee Benefit Application Detail,Détail de la demande d'avantages sociaux, Earning Component,Composante de revenu, Pay Against Benefit Claim,Payer la demande de prestations, Max Benefit Amount,Montant maximal des prestations, @@ -6420,15 +6420,15 @@ Claim Benefit For,Demande de prestations pour, Max Amount Eligible,Montant maximum admissible, Expense Proof,Preuves de dépenses, Employee Boarding Activity,Activité d'intégration des nouveaux employés, -Activity Name,Nom de l'activité, +Activity Name,Nom de l'activité, Task Weight,Poids de la Tâche, -Required for Employee Creation,Obligatoire pour la création d'un employé, +Required for Employee Creation,Obligatoire pour la création d'un employé, Applicable in the case of Employee Onboarding,Applicable dans le cas de l'accueil des nouveaux employés, Employee Checkin,Enregistrement des employés, Log Type,Type de journal, OUT,EN DEHORS, Location / Device ID,Emplacement / ID de périphérique, -Skip Auto Attendance,Ignorer l'assistance automatique, +Skip Auto Attendance,Ignorer l'assistance automatique, Shift Start,Début de quart, Shift End,Fin de quart, Shift Actual Start,Décalage début effectif, @@ -6445,7 +6445,7 @@ Employee External Work History,Antécédents Professionnels de l'Employé, Total Experience,Expérience Totale, Default Leave Policy,Politique de congés par défaut, Default Salary Structure,Structure salariale par défaut, -Employee Group Table,Table de groupe d'employés, +Employee Group Table,Table de groupe d'employés, ERPNext User ID,ID utilisateur ERPNext, Employee Health Insurance,Assurance maladie des employés, Health Insurance Name,Nom de l'assurance santé, @@ -6465,34 +6465,34 @@ Employee Property History,Historique des propriétés des champs de la fiche emp Employee Separation,Départ des employés, Employee Separation Template,Modèle de départ des employés, Exit Interview Summary,Récapitulatif de l'entretien de sortie, -Employee Skill,Compétence de l'employé, +Employee Skill,Compétence de l'employé, Proficiency,Compétence, -Evaluation Date,Date d'évaluation, +Evaluation Date,Date d'évaluation, Employee Skill Map,Carte de compétences des employés, Employee Skills,Compétences des employés, Trainings,Des formations, Employee Tax Exemption Category,Catégorie d'exemption de taxe des employés, -Max Exemption Amount,Montant maximum d'exemption, +Max Exemption Amount,Montant maximum d'exemption, Employee Tax Exemption Declaration,Déclaration d'exemption de taxe, Declarations,Déclarations, Total Declared Amount,Montant total déclaré, Total Exemption Amount,Montant total de l'exonération, Employee Tax Exemption Declaration Category,Catégorie de déclaration d'exemption de taxe, -Exemption Sub Category,Sous-catégorie d'exemption, -Exemption Category,Catégorie d'exemption, +Exemption Sub Category,Sous-catégorie d'exemption, +Exemption Category,Catégorie d'exemption, Maximum Exempted Amount,Montant maximum exonéré, Declared Amount,Montant Déclaré, Employee Tax Exemption Proof Submission,Soumission d'une preuve d'exemption de taxe, Submission Date,Date de soumission, -Tax Exemption Proofs,Preuves d'exonération fiscale, +Tax Exemption Proofs,Preuves d'exonération fiscale, Total Actual Amount,Montant total total, Employee Tax Exemption Proof Submission Detail,Détails de la soumission de preuve d'exemption de taxe, -Maximum Exemption Amount,Montant maximum d'exemption, +Maximum Exemption Amount,Montant maximum d'exemption, Type of Proof,Type de preuve, Actual Amount,Montant actuel, Employee Tax Exemption Sub Category,Sous-catégorie d'exemption de taxe, Tax Exemption Category,Catégorie d'exonération fiscale, -Employee Training,Entrainement d'employé, +Employee Training,Entrainement d'employé, Training Date,Date de formation, Employee Transfer,Transfert des employés, Transfer Date,Date de transfert, @@ -6505,7 +6505,7 @@ Employee Transfer Property,Propriété des champs pour le transfert des employé HR-EXP-.YYYY.-,HR-EXP-. AAAA.-, Expense Taxes and Charges,Frais et taxes, Total Sanctioned Amount,Montant Total Validé, -Total Advance Amount,Montant total de l'avance, +Total Advance Amount,Montant total de l'avance, Total Claimed Amount,Montant Total Réclamé, Total Amount Reimbursed,Montant Total Remboursé, Vehicle Log,Journal du Véhicule, @@ -6540,7 +6540,7 @@ Include holidays in Total no. of Working Days,Inclure les vacances dans le nombr Email Salary Slip to Employee,Envoyer la Fiche de Paie à l'Employé par Mail, Emails salary slip to employee based on preferred email selected in Employee,Envoi des fiches de paie à l'employé par Email en fonction de l'email sélectionné dans la fiche Employé, Encrypt Salary Slips in Emails,Crypter les bulletins de salaire dans les courriels, -"The salary slip emailed to the employee will be password protected, the password will be generated based on the password policy.",La fiche de salaire envoyée à l'employé par courrier électronique sera protégée par un mot de passe. Le mot de passe sera généré en fonction de la politique de mot de passe., +"The salary slip emailed to the employee will be password protected, the password will be generated based on the password policy.",La fiche de salaire envoyée à l'employé par courrier électronique sera protégée par un mot de passe. Le mot de passe sera généré en fonction de la politique de mot de passe., Password Policy,Politique de mot de passe, Example: SAL-{first_name}-{date_of_birth.year}
    This will generate a password like SAL-Jane-1972,Exemple: SAL- {prenom} - {date_naissance.année}
    Cela générera un mot de passe comme SAL-Jane-1972, Leave Settings,Paramètres des Congés, @@ -6551,10 +6551,10 @@ Leave Approver Mandatory In Leave Application,Approbateur de congés obligatoire Show Leaves Of All Department Members In Calendar,Afficher les congés de tous les membres du département dans le calendrier, Auto Leave Encashment,Auto Leave Encashment, Restrict Backdated Leave Application,Restreindre la demande de congé antidaté, -Hiring Settings,Paramètres d'embauche, -Check Vacancies On Job Offer Creation,Vérifier les offres d'emploi lors de la création d'une offre d'emploi, -Identification Document Type,Type de document d'identification, -Standard Tax Exemption Amount,Montant de l'exemption fiscale standard, +Hiring Settings,Paramètres d'embauche, +Check Vacancies On Job Offer Creation,Vérifier les offres d'emploi lors de la création d'une offre d'emploi, +Identification Document Type,Type de document d'identification, +Standard Tax Exemption Amount,Montant de l'exemption fiscale standard, Taxable Salary Slabs,Paliers de salaire imposables, Applicant for a Job,Candidat à un Emploi, Accepted,Accepté, @@ -6606,11 +6606,11 @@ Leave Block List Date,Date de la Liste de Blocage des Congés, Block Date,Bloquer la Date, Leave Control Panel,Quitter le Panneau de Configuration, Select Employees,Sélectionner les Employés, -Employment Type (optional),Type d'emploi (facultatif), +Employment Type (optional),Type d'emploi (facultatif), Branch (optional),Branche (optionnel), Department (optional),Département (optionnel), Designation (optional),Désignation (facultatif), -Employee Grade (optional),Grade d'employé (facultatif), +Employee Grade (optional),Grade d'employé (facultatif), Employee (optional),Employé (facultatif), Allocate Leaves,Allouer des feuilles, Carry Forward,Reporter, @@ -6642,8 +6642,8 @@ Maximum Carry Forwarded Leaves,Nombre maximal de congés reportés, Expire Carry Forwarded Leaves (Days),Expirer les congés reportés (jours), Calculated in days,Calculé en jours, Encashment,Encaissement, -Allow Encashment,Autoriser l'encaissement, -Encashment Threshold Days,Jours de seuil d'encaissement, +Allow Encashment,Autoriser l'encaissement, +Encashment Threshold Days,Jours de seuil d'encaissement, Earned Leave,Congés acquis, Is Earned Leave,Est un congé acquis, Earned Leave Frequency,Fréquence d'acquisition des congés, @@ -6653,7 +6653,7 @@ Payroll Frequency,Fréquence de la Paie, Fortnightly,Bimensuel, Bimonthly,Bimensuel, Employees,Employés, -Number Of Employees,Nombre d'employés, +Number Of Employees,Nombre d'employés, Employee Details,Détails des employés, Validate Attendance,Valider la présence, Salary Slip Based on Timesheet,Fiche de Paie basée sur la Feuille de Temps, @@ -6673,9 +6673,9 @@ Abbr,Abré, Depends on Payment Days,Dépend des jours de paiement, Is Tax Applicable,Est taxable, Variable Based On Taxable Salary,Variable basée sur le salaire imposable, -Round to the Nearest Integer,Arrondir à l'entier le plus proche, +Round to the Nearest Integer,Arrondir à l'entier le plus proche, Statistical Component,Composante Statistique, -"If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ","Si cette option est sélectionnée, la valeur spécifiée ou calculée dans ce composant ne contribuera pas aux gains ou aux déductions. Cependant, sa valeur peut être référencée par d'autres composants qui peuvent être ajoutés ou déduits.", +"If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ","Si cette option est sélectionnée, la valeur spécifiée ou calculée dans ce composant ne contribuera pas aux gains ou aux déductions. Cependant, sa valeur peut être référencée par d'autres composants qui peuvent être ajoutés ou déduits.", Flexible Benefits,Avantages sociaux variables, Is Flexible Benefit,Est un avantage flexible, Max Benefit Amount (Yearly),Montant maximum des prestations sociales (annuel), @@ -6703,7 +6703,7 @@ Earnings,Bénéfices, Deductions,Déductions, Employee Loan,Prêt Employé, Total Principal Amount,Montant total du capital, -Total Interest Amount,Montant total de l'intérêt, +Total Interest Amount,Montant total de l'intérêt, Total Loan Repayment,Total de Remboursement du Prêt, net pay info,Info de salaire net, Gross Pay - Total Deduction - Loan Repayment,Salaire Brut - Déductions Totales - Remboursement de Prêt, @@ -6719,17 +6719,17 @@ Shift Assignment,Affectation de quart, Shift Type,Type de quart, Shift Request,Demande de quart, Enable Auto Attendance,Activer la présence automatique, -Mark attendance based on 'Employee Checkin' for Employees assigned to this shift.,Marquez la présence sur la base de 'Enregistrement des employés' pour les employés affectés à ce poste., +Mark attendance based on 'Employee Checkin' for Employees assigned to this shift.,Marquez la présence sur la base de 'Enregistrement des employés' pour les employés affectés à ce poste., Auto Attendance Settings,Paramètres de présence automatique, Determine Check-in and Check-out,Déterminer les entrées et les sorties, Alternating entries as IN and OUT during the same shift,Alterner les entrées comme IN et OUT pendant le même quart, -Strictly based on Log Type in Employee Checkin,Strictement basé sur le type de journal dans l'enregistrement des employés, +Strictly based on Log Type in Employee Checkin,Strictement basé sur le type de journal dans l'enregistrement des employés, Working Hours Calculation Based On,Calcul des heures de travail basé sur, First Check-in and Last Check-out,Premier enregistrement et dernier départ, Every Valid Check-in and Check-out,Chaque enregistrement valide et check-out, -Begin check-in before shift start time (in minutes),Commencez l'enregistrement avant l'heure de début du poste (en minutes), -The time before the shift start time during which Employee Check-in is considered for attendance.,Heure avant l'heure de début du quart pendant laquelle l'enregistrement des employés est pris en compte pour la présence., -Allow check-out after shift end time (in minutes),Autoriser le départ après l'heure de fin du quart (en minutes), +Begin check-in before shift start time (in minutes),Commencez l'enregistrement avant l'heure de début du poste (en minutes), +The time before the shift start time during which Employee Check-in is considered for attendance.,Heure avant l'heure de début du quart pendant laquelle l'enregistrement des employés est pris en compte pour la présence., +Allow check-out after shift end time (in minutes),Autoriser le départ après l'heure de fin du quart (en minutes), Time after the end of shift during which check-out is considered for attendance.,Heure après la fin du quart de travail au cours de laquelle la prise en charge est prise en compte., Working Hours Threshold for Half Day,Seuil des heures de travail pour une demi-journée, Working hours below which Half Day is marked. (Zero to disable),Heures de travail en dessous desquelles la demi-journée est marquée. (Zéro à désactiver), @@ -6738,14 +6738,14 @@ Working hours below which Absent is marked. (Zero to disable),Heures de travail Process Attendance After,Processus de présence après, Attendance will be marked automatically only after this date.,La participation sera automatiquement marquée après cette date., Last Sync of Checkin,Dernière synchronisation de Checkin, -Last Known Successful Sync of Employee Checkin. Reset this only if you are sure that all Logs are synced from all the locations. Please don't modify this if you are unsure.,Dernière synchronisation réussie de l'enregistrement des employés. Réinitialisez cette opération uniquement si vous êtes certain que tous les journaux sont synchronisés à partir de tous les emplacements. S'il vous plaît ne modifiez pas cela si vous n'êtes pas sûr., -Grace Period Settings For Auto Attendance,Paramètres de période de grâce pour l'assistance automatique, -Enable Entry Grace Period,Activer la période de grâce d'entrée, +Last Known Successful Sync of Employee Checkin. Reset this only if you are sure that all Logs are synced from all the locations. Please don't modify this if you are unsure.,Dernière synchronisation réussie de l'enregistrement des employés. Réinitialisez cette opération uniquement si vous êtes certain que tous les journaux sont synchronisés à partir de tous les emplacements. S'il vous plaît ne modifiez pas cela si vous n'êtes pas sûr., +Grace Period Settings For Auto Attendance,Paramètres de période de grâce pour l'assistance automatique, +Enable Entry Grace Period,Activer la période de grâce d'entrée, Late Entry Grace Period,Délai de grâce pour entrée tardive, -The time after the shift start time when check-in is considered as late (in minutes).,L'heure après l'heure de début du quart de travail où l'enregistrement est considéré comme tardif (en minutes)., +The time after the shift start time when check-in is considered as late (in minutes).,L'heure après l'heure de début du quart de travail où l'enregistrement est considéré comme tardif (en minutes)., Enable Exit Grace Period,Activer la période de grâce de sortie, Early Exit Grace Period,Période de grâce de sortie anticipée, -The time before the shift end time when check-out is considered as early (in minutes).,L'heure avant l'heure de fin du quart de travail au moment du départ est considérée comme précoce (en minutes)., +The time before the shift end time when check-out is considered as early (in minutes).,L'heure avant l'heure de fin du quart de travail au moment du départ est considérée comme précoce (en minutes)., Skill Name,Nom de la compétence, Staffing Plan Details,Détails du plan de dotation, Staffing Plan Detail,Détail du plan de dotation, @@ -6797,8 +6797,8 @@ Travel Advance Required,Avance de déplacement requise, Departure Datetime,Date/Heure de départ, Arrival Datetime,Date/Heure d'arrivée, Lodging Required,Hébergement requis, -Preferred Area for Lodging,Zone préférée pour l'hébergement, -Check-in Date,Date d'arrivée, +Preferred Area for Lodging,Zone préférée pour l'hébergement, +Check-in Date,Date d'arrivée, Check-out Date,Date de départ, Travel Request,Demande de déplacement, Travel Type,Type de déplacement, @@ -6808,15 +6808,15 @@ Travel Funding,Financement du déplacement, Require Full Funding,Nécessite un financement complet, Fully Sponsored,Entièrement commandité, "Partially Sponsored, Require Partial Funding","Partiellement sponsorisé, nécessite un financement partiel", -Copy of Invitation/Announcement,Copie de l'invitation / annonce, +Copy of Invitation/Announcement,Copie de l'invitation / annonce, "Details of Sponsor (Name, Location)","Détails du commanditaire (nom, lieu)", Identification Document Number,Numéro du document d'identification, Any other details,Tout autre détail, Costing Details,Détails des coûts, Costing,Coût, -Event Details,Détails de l'évènement, -Name of Organizer,Nom de l'organisateur, -Address of Organizer,Adresse de l'organisateur, +Event Details,Détails de l'évènement, +Name of Organizer,Nom de l'organisateur, +Address of Organizer,Adresse de l'organisateur, Travel Request Costing,Coût de la demande de déplacement, Expense Type,Type de dépense, Sponsored Amount,Montant sponsorisé, @@ -6865,14 +6865,14 @@ Inspection,Inspection, Mileage,Kilométrage, Hub Tracked Item,Article suivi sur le Hub, Hub Node,Noeud du Hub, -Image List,Liste d'images, +Image List,Liste d'images, Item Manager,Gestionnaire d'Article, Hub User,Utilisateur du hub, Hub Password,Mot de passe Hub, Hub Users,Utilisateurs du Hub, Marketplace Settings,Paramètres du marché, Disable Marketplace,Désactiver le marché, -Marketplace URL (to hide and update label),URL du marché (pour masquer et mettre à jour l'étiquette), +Marketplace URL (to hide and update label),URL du marché (pour masquer et mettre à jour l'étiquette), Registered,Inscrit, Sync in Progress,Synchronisation en cours, Hub Seller Name,Nom du vendeur, @@ -6946,8 +6946,8 @@ Unpledge Time,Désengager le temps, Unpledge Type,Type de désengagement, Loan Name,Nom du Prêt, Rate of Interest (%) Yearly,Taux d'Intérêt (%) Annuel, -Penalty Interest Rate (%) Per Day,Taux d'intérêt de pénalité (%) par jour, -Penalty Interest Rate is levied on the pending interest amount on a daily basis in case of delayed repayment ,Le taux d'intérêt de pénalité est prélevé quotidiennement sur le montant des intérêts en attente en cas de retard de remboursement, +Penalty Interest Rate (%) Per Day,Taux d'intérêt de pénalité (%) par jour, +Penalty Interest Rate is levied on the pending interest amount on a daily basis in case of delayed repayment ,Le taux d'intérêt de pénalité est prélevé quotidiennement sur le montant des intérêts en attente en cas de retard de remboursement, Grace Period in Days,Délai de grâce en jours, Pledge,Gage, Post Haircut Amount,Montant de la coupe de cheveux, @@ -6961,7 +6961,7 @@ Loan Repayment Entry,Entrée de remboursement de prêt, Sanctioned Loan Amount,Montant du prêt sanctionné, Sanctioned Amount Limit,Limite de montant sanctionnée, Unpledge,Désengager, -Against Pledge,Contre l'engagement, +Against Pledge,Contre l'engagement, Haircut,la Coupe de cheveux, MAT-MSH-.YYYY.-,MAT-MSH-YYYY.-, Generate Schedule,Créer un Échéancier, @@ -7002,7 +7002,7 @@ Transfer Material Against,Transférer du matériel contre, Routing,Routage, Materials,Matériels, Quality Inspection Required,Inspection de qualité requise, -Quality Inspection Template,Modèle d'inspection de la qualité, +Quality Inspection Template,Modèle d'inspection de la qualité, Scrap,Mettre au Rebut, Scrap Items,Mettre au Rebut des Articles, Operating Cost,Coût d'Exploitation, @@ -7012,7 +7012,7 @@ Operating Cost (Company Currency),Coût d'Exploitation (Devise Société), Raw Material Cost (Company Currency),Coût de la matière première (devise de la société), Scrap Material Cost(Company Currency),Coût de Mise au Rebut des Matériaux (Devise Société), Total Cost,Coût Total, -Total Cost (Company Currency),Coût total (devise de l'entreprise), +Total Cost (Company Currency),Coût total (devise de l'entreprise), Materials Required (Exploded),Matériel Requis (Éclaté), Exploded Items,Articles éclatés, Item Image (if not slideshow),Image de l'Article (si ce n'est diaporama), @@ -7023,9 +7023,9 @@ Show Operations,Afficher Opérations, Website Description,Description du Site Web, BOM Explosion Item,Article Eclaté LDM, Qty Consumed Per Unit,Qté Consommée Par Unité, -Include Item In Manufacturing,Inclure l'article dans la fabrication, +Include Item In Manufacturing,Inclure l'article dans la fabrication, BOM Item,Article LDM, -Item operation,Opération de l'article, +Item operation,Opération de l'article, Rate & Amount,Taux et Montant, Basic Rate (Company Currency),Taux de Base (Devise de la Société ), Scrap %,% de Rebut, @@ -7118,24 +7118,24 @@ Product Bundle Item,Article d'un Ensemble de Produits, Production Plan Material Request,Demande de Matériel du Plan de Production, Production Plan Sales Order,Commande Client du Plan de Production, Sales Order Date,Date de la Commande Client, -Routing Name,Nom d'acheminement, +Routing Name,Nom d'acheminement, MFG-WO-.YYYY.-,MFG-WO-.YYYY.-, Item To Manufacture,Article à produire, Material Transferred for Manufacturing,Matériel Transféré pour la Production, Manufactured Qty,Qté Produite, Use Multi-Level BOM,Utiliser LDM à Plusieurs Niveaux, Plan material for sub-assemblies,Plan de matériaux pour les sous-ensembles, -Skip Material Transfer to WIP Warehouse,Ignorer le transfert de matériel vers l'entrepôt WIP, +Skip Material Transfer to WIP Warehouse,Ignorer le transfert de matériel vers l'entrepôt WIP, Check if material transfer entry is not required,Vérifiez si une un transfert de matériel n'est pas requis, -Backflush Raw Materials From Work-in-Progress Warehouse,Rembourrage des matières premières dans l'entrepôt de travaux en cours, +Backflush Raw Materials From Work-in-Progress Warehouse,Rembourrage des matières premières dans l'entrepôt de travaux en cours, Update Consumed Material Cost In Project,Mettre à jour le coût des matières consommées dans le projet, Warehouses,Entrepôts, -This is a location where raw materials are available.,C'est un endroit où les matières premières sont disponibles., +This is a location where raw materials are available.,C'est un endroit où les matières premières sont disponibles., Work-in-Progress Warehouse,Entrepôt des Travaux en Cours, -This is a location where operations are executed.,Il s'agit d'un emplacement où les opérations sont exécutées., -This is a location where final product stored.,Il s'agit d'un emplacement où le produit final est stocké., +This is a location where operations are executed.,Il s'agit d'un emplacement où les opérations sont exécutées., +This is a location where final product stored.,Il s'agit d'un emplacement où le produit final est stocké., Scrap Warehouse,Entrepôt de Rebut, -This is a location where scraped materials are stored.,Il s'agit d'un emplacement où les matériaux raclés sont stockés., +This is a location where scraped materials are stored.,Il s'agit d'un emplacement où les matériaux raclés sont stockés., Required Items,Articles Requis, Actual Start Date,Date de Début Réelle, Planned End Date,Date de Fin Prévue, @@ -7205,13 +7205,13 @@ Grant Description,Description de la subvention, Requested Amount,Quantité exigée, Has any past Grant Record,A obtenu des bourses par le passé, Show on Website,Afficher sur le site Web, -Assessment Mark (Out of 10),Note d'évaluation (sur 10), -Assessment Manager,Gestionnaire d'évaluation, +Assessment Mark (Out of 10),Note d'évaluation (sur 10), +Assessment Manager,Gestionnaire d'évaluation, Email Notification Sent,Notification par e-mail envoyée, NPO-MEM-.YYYY.-,NPO-MEM-YYYY.-, -Membership Expiry Date,Date d'expiration de l'adhésion, +Membership Expiry Date,Date d'expiration de l'adhésion, Non Profit Member,Membre de l'association, -Membership Status,Statut d'adhésion, +Membership Status,Statut d'adhésion, Member Since,Membre depuis, Volunteer Name,Nom du bénévole, Volunteer Type,Type de bénévole, @@ -7227,12 +7227,12 @@ Volunteer Skills,Compétences bénévoles, Volunteer Skill,Compétence bénévole, Homepage,Page d'Accueil, Hero Section Based On,Section de héros basée sur, -Homepage Section,Section de la page d'accueil, +Homepage Section,Section de la page d'accueil, Hero Section,Section de héros, Tag Line,Ligne de Tag, Company Tagline for website homepage,Slogan de la Société pour la page d'accueil du site web, Company Description for website homepage,Description de la Société pour la page d'accueil du site web, -Homepage Slideshow,Diaporama de la page d'accueil, +Homepage Slideshow,Diaporama de la page d'accueil, "URL for ""All Products""","URL pour ""Tous les Produits""", Products to be shown on website homepage,Produits destinés à être affichés sur la page d’accueil du site web, Homepage Featured Product,Produit Présenté sur la Page d'Accueil, @@ -7249,12 +7249,12 @@ Subtitle,Sous-titre, Products Settings,Paramètres des Produits, Home Page is Products,La Page d'Accueil est Produits, "If checked, the Home page will be the default Item Group for the website","Si cochée, la page d'Accueil pour le site sera le Groupe d'Article par défaut", -Show Availability Status,Afficher l'état de la disponibilité, +Show Availability Status,Afficher l'état de la disponibilité, Product Page,Page produit, Products per Page,Produits par page, Enable Field Filters,Activer les filtres de champ, -Item Fields,Champs de l'article, -Enable Attribute Filters,Activer les filtres d'attributs, +Item Fields,Champs de l'article, +Enable Attribute Filters,Activer les filtres d'attributs, Attributes,Attributs, Hide Variants,Masquer les variantes, Website Attribute,Attribut de site Web, @@ -7391,7 +7391,7 @@ October,octobre, November,novembre, December,décembre, JSON Output,Sortie JSON, -Invoices with no Place Of Supply,Factures sans lieu d'approvisionnement, +Invoices with no Place Of Supply,Factures sans lieu d'approvisionnement, Import Supplier Invoice,Importer la facture fournisseur, Invoice Series,Série de factures, Upload XML Invoices,Télécharger des factures XML, @@ -7424,7 +7424,7 @@ SAL-CAM-.YYYY.-,SAL-CAM-YYYY.-, Campaign Schedules,Horaires de campagne, Buyer of Goods and Services.,Acheteur des Biens et Services., CUST-.YYYY.-,CUST-.YYYY.-, -Default Company Bank Account,Compte bancaire d'entreprise par défaut, +Default Company Bank Account,Compte bancaire d'entreprise par défaut, From Lead,Du Prospect, Account Manager,Gestionnaire de compte, Default Price List,Liste des Prix par Défaut, @@ -7466,7 +7466,7 @@ POS Closing Voucher Details,Détail du bon de clôture du PDV, Collected Amount,Montant collecté, Expected Amount,Montant prévu, POS Closing Voucher Invoices,Factures du bon de clôture du PDV, -Quantity of Items,Quantité d'articles, +Quantity of Items,Quantité d'articles, POS Closing Voucher Taxes,Taxes du bon de clotûre du PDV, "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have ""Is Stock Item"" as ""No"" and ""Is Sales Item"" as ""Yes"".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials","Regroupement d' **Articles** dans un autre **Article**. Ceci est utile si vous regroupez certains **Articles** dans un lot et que vous maintenez l'inventaire des **Articles** du lot et non de l'**Article** composé. L'**Article** composé aura ""Article En Stock"" à ""Non"" et ""Article À Vendre"" à ""Oui"". Exemple : Si vous vendez des Ordinateurs Portables et Sacs à Dos séparément et qu'il y a un prix spécial si le client achète les deux, alors l'Ordinateur Portable + le Sac à Dos sera un nouveau Produit Groupé. Remarque: LDM = Liste\nDes Matériaux", Parent Item,Article Parent, @@ -7564,7 +7564,7 @@ Default Values,Valeurs Par Défaut, Default Holiday List,Liste de Vacances par Défaut, Standard Working Hours,Heures de travail standard, Default Selling Terms,Conditions de vente par défaut, -Default Buying Terms,Conditions d'achat par défaut, +Default Buying Terms,Conditions d'achat par défaut, Default warehouse for Sales Return,Magasin par défaut pour retour de vente, Create Chart Of Accounts Based On,Créer un Plan Comptable Basé Sur, Standard Template,Modèle Standard, @@ -7579,8 +7579,8 @@ Total Monthly Sales,Total des Ventes Mensuelles, Default Cash Account,Compte de Caisse par Défaut, Default Receivable Account,Compte Client par Défaut, Round Off Cost Center,Centre de Coûts d’Arrondi, -Discount Allowed Account,Compte d'escompte autorisé, -Discount Received Account,Compte d'escompte reçu, +Discount Allowed Account,Compte d'escompte autorisé, +Discount Received Account,Compte d'escompte reçu, Exchange Gain / Loss Account,Compte de Profits / Pertes sur Change, Unrealized Exchange Gain/Loss Account,Compte de gains / pertes de change non réalisés, Allow Account Creation Against Child Company,Autoriser la création de compte contre une entreprise enfant, @@ -7608,7 +7608,7 @@ Company Logo,Logo de la société, Date of Incorporation,Date de constitution, Date of Commencement,Date de démarrage, Phone No,N° de Téléphone, -Company Description,Description de l'entreprise, +Company Description,Description de l'entreprise, Registration Details,Informations Légales, Company registration numbers for your reference. Tax numbers etc.,"Numéro d'immatriculation de la Société pour votre référence. Numéros de taxes, etc.", Delete Company Transactions,Supprimer les Transactions de la Société, @@ -7639,15 +7639,15 @@ Bank Credit Balance,Solde bancaire, Receivables,Créances, Payables,Dettes, Sales Orders to Bill,Commandes de vente à facture, -Purchase Orders to Bill,Commandes d'achat à facturer, +Purchase Orders to Bill,Commandes d'achat à facturer, New Sales Orders,Nouvelles Commandes Client, New Purchase Orders,Nouveaux Bons de Commande, Sales Orders to Deliver,Commandes de vente à livrer, -Purchase Orders to Receive,Commandes d'achat à recevoir, -New Purchase Invoice,Nouvelle facture d'achat, +Purchase Orders to Receive,Commandes d'achat à recevoir, +New Purchase Invoice,Nouvelle facture d'achat, New Quotations,Nouveaux Devis, Open Quotations,Citations ouvertes, -Purchase Orders Items Overdue,Articles de commandes d'achat en retard, +Purchase Orders Items Overdue,Articles de commandes d'achat en retard, Add Quote,Ajouter une Citation, Global Defaults,Valeurs par Défaut Globales, Default Company,Société par Défaut, @@ -7662,7 +7662,7 @@ Item Classification,Classification de l'Article, General Settings,Paramètres Généraux, Item Group Name,Nom du Groupe d'Article, Parent Item Group,Groupe d’Articles Parent, -Item Group Defaults,Groupe d'articles par défaut, +Item Group Defaults,Groupe d'articles par défaut, Item Tax,Taxe sur l'Article, Check this if you want to show in website,Cochez cette case si vous souhaitez afficher sur le site, Show this slideshow at the top of the page,Afficher ce diaporama en haut de la page, @@ -7723,7 +7723,7 @@ Website Item Group,Groupe d'Articles du Site Web, Cross Listing of Item in multiple groups,Liste Croisée d'Articles dans plusieurs groupes, Default settings for Shopping Cart,Paramètres par défaut pour le Panier d'Achat, Enable Shopping Cart,Activer Panier, -Display Settings,Paramètres d'affichage, +Display Settings,Paramètres d'affichage, Show Public Attachments,Afficher les Pièces Jointes Publiques, Show Price,Afficher le prix, Show Stock Availability,Afficher la disponibilité du stock, @@ -7756,7 +7756,7 @@ Tariff Number,Tarif, Delivery To,Livraison à, MAT-DN-.YYYY.-,MAT-DN-.YYYY.-, Is Return,Est un Retour, -Issue Credit Note,Note de crédit d'émission, +Issue Credit Note,Note de crédit d'émission, Return Against Delivery Note,Retour contre Bon de Livraison, Customer's Purchase Order No,Numéro bon de commande du client, Billing Address Name,Nom de l'Adresse de Facturation, @@ -7783,8 +7783,8 @@ Available Batch Qty at From Warehouse,Qté de Lot Disponible Depuis l'Entrepôt, Available Qty at From Warehouse,Qté Disponible Depuis l'Entrepôt, Delivery Settings,Paramètres de livraison, Dispatch Settings,Paramètres de répartition, -Dispatch Notification Template,Modèle de notification d'expédition, -Dispatch Notification Attachment,Pièce jointe de notification d'expédition, +Dispatch Notification Template,Modèle de notification d'expédition, +Dispatch Notification Attachment,Pièce jointe de notification d'expédition, Leave blank to use the standard Delivery Note format,Laissez vide pour utiliser le format de bon de livraison standard, Send with Attachment,Envoyer avec pièce jointe, Delay between Delivery Stops,Délai entre les arrêts de livraison, @@ -7793,7 +7793,7 @@ Visited,Visité, Order Information,Informations sur la commande, Contact Information,Informations de contact, Email sent to,Email Envoyé À, -Dispatch Information,Informations d'expédition, +Dispatch Information,Informations d'expédition, Estimated Arrival,Arrivée estimée, MAT-DT-.YYYY.-,MAT-DT-.YYYY.-, Initial Email Notification Sent,Notification initiale par e-mail envoyée, @@ -7804,10 +7804,10 @@ Total Estimated Distance,Distance totale estimée, Distance UOM,Distance UOM, Departure Time,Heure de départ, Delivery Stops,Étapes de Livraison, -Calculate Estimated Arrival Times,Calculer les heures d'arrivée estimées, -Use Google Maps Direction API to calculate estimated arrival times,Utiliser l'API Google Maps Direction pour calculer les heures d'arrivée estimées, -Optimize Route,Optimiser l'itinéraire, -Use Google Maps Direction API to optimize route,Utiliser l'API Google Maps Direction pour optimiser l'itinéraire, +Calculate Estimated Arrival Times,Calculer les heures d'arrivée estimées, +Use Google Maps Direction API to calculate estimated arrival times,Utiliser l'API Google Maps Direction pour calculer les heures d'arrivée estimées, +Optimize Route,Optimiser l'itinéraire, +Use Google Maps Direction API to optimize route,Utiliser l'API Google Maps Direction pour optimiser l'itinéraire, In Transit,En transit, Fulfillment User,Livreur, "A Product or a Service that is bought, sold or kept in stock.","Un Produit ou un Service qui est acheté, vendu ou conservé en stock.", @@ -7817,7 +7817,7 @@ Is Item from Hub,Est un article sur le Hub, Default Unit of Measure,Unité de Mesure par Défaut, Maintain Stock,Maintenir Stock, Standard Selling Rate,Prix de Vente Standard, -Auto Create Assets on Purchase,Création automatique d'actifs à l'achat, +Auto Create Assets on Purchase,Création automatique d'actifs à l'achat, Asset Naming Series,Nom de série de l'actif, Over Delivery/Receipt Allowance (%),Surlivrance / indemnité de réception (%), Barcodes,Codes-barres, @@ -7839,9 +7839,9 @@ Automatically Create New Batch,Créer un Nouveau Lot Automatiquement, Batch Number Series,Série de numéros de lots, "Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si la série est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec cette série. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe de la série dans les paramètres de stock.", Has Expiry Date,A une date d'expiration, -Retain Sample,Conserver l'échantillon, -Max Sample Quantity,Quantité maximum d'échantillon, -Maximum sample quantity that can be retained,Quantité maximale d'échantillon pouvant être conservée, +Retain Sample,Conserver l'échantillon, +Max Sample Quantity,Quantité maximum d'échantillon, +Maximum sample quantity that can be retained,Quantité maximale d'échantillon pouvant être conservée, Has Serial No,A un N° de Série, Serial Number Series,Séries de Numéros de Série, "Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.","Exemple:. ABCD ##### Si la série est définie et que le N° de série n'est pas mentionné dans les transactions, alors un numéro de série automatique sera créé basé sur cette série. Si vous voulez toujours mentionner explicitement les numéros de série pour ce produit. laissez ce champ vide.", @@ -7852,13 +7852,13 @@ Variant Based On,Variante Basée Sur, Item Attribute,Attribut de l'Article, "Sales, Purchase, Accounting Defaults","Valeurs par défaut pour les ventes, les achats et la comptabilité", Item Defaults,Paramètres par défaut de l'article, -"Purchase, Replenishment Details","Détails d'achat, de réapprovisionnement", +"Purchase, Replenishment Details","Détails d'achat, de réapprovisionnement", Is Purchase Item,Est Article d'Achat, Default Purchase Unit of Measure,Unité de Mesure par défaut à l'Achat, Minimum Order Qty,Qté de Commande Minimum, -Minimum quantity should be as per Stock UOM,La quantité minimale doit être conforme à l'UdM du stock, +Minimum quantity should be as per Stock UOM,La quantité minimale doit être conforme à l'UdM du stock, Average time taken by the supplier to deliver,Délai moyen de livraison par le fournisseur, -Is Customer Provided Item,L'article est-il fourni par le client?, +Is Customer Provided Item,L'article est-il fourni par le client?, Delivered by Supplier (Drop Ship),Livré par le Fournisseur (Expédition Directe), Supplier Items,Articles Fournisseur, Foreign Trade Details,Détails du Commerce Extérieur, @@ -7886,7 +7886,7 @@ Website Item Groups,Groupes d'Articles du Site Web, List this Item in multiple groups on the website.,Liste cet article dans plusieurs groupes sur le site., Copy From Item Group,Copier Depuis un Groupe d'Articles, Website Content,Contenu du site Web, -You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.,Vous pouvez utiliser n'importe quelle balise Bootstrap 4 valide dans ce champ. Il sera affiché sur votre page d'article., +You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.,Vous pouvez utiliser n'importe quelle balise Bootstrap 4 valide dans ce champ. Il sera affiché sur votre page d'article., Total Projected Qty,Qté Totale Prévue, Hub Publishing Details,Détails Publiés sur le Hub, Publish in Hub,Publier dans le Hub, @@ -7898,7 +7898,7 @@ Synced With Hub,Synchronisé avec le Hub, Item Alternative,Alternative à l'Article, Alternative Item Code,Code de l'article alternatif, Two-way,A double-sens, -Alternative Item Name,Nom de l'article alternatif, +Alternative Item Name,Nom de l'article alternatif, Attribute Name,Nom de l'Attribut, Numeric Values,Valeurs Numériques, From Range,Plage Initiale, @@ -7925,7 +7925,7 @@ Sales Defaults,Valeurs par défaut pour la vente, Default Selling Cost Center,Centre de Coût Vendeur par Défaut, Item Manufacturer,Fabricant d'Article, Item Price,Prix de l'Article, -Packing Unit,Unité d'emballage, +Packing Unit,Unité d'emballage, Quantity that must be bought or sold per UOM,Quantité à acheter ou à vendre par unité de mesure, Valid From ,Valide à Partir de, Valid Upto ,Valide Jusqu'au, @@ -8001,7 +8001,7 @@ Pick List Item,Élément de la liste de choix, Picked Qty,Quantité choisie, Price List Master,Données de Base des Listes de Prix, Price List Name,Nom de la Liste de Prix, -Price Not UOM Dependent,Prix non dépendant de l'UOM, +Price Not UOM Dependent,Prix non dépendant de l'UOM, Applicable for Countries,Applicable pour les Pays, Price List Country,Pays de la Liste des Prix, MAT-PRE-.YYYY.-,MAT-PRE-YYYY.-, @@ -8018,7 +8018,7 @@ Vehicle Date,Date du Véhicule, Received and Accepted,Reçus et Acceptés, Accepted Quantity,Quantité Acceptée, Rejected Quantity,Quantité Rejetée, -Sample Quantity,Quantité d'échantillon, +Sample Quantity,Quantité d'échantillon, Rate and Amount,Prix et Montant, MAT-QA-.YYYY.-,MAT-QA-YYYY.-, Report Date,Date du Rapport, @@ -8038,7 +8038,7 @@ Reading 7,Lecture 7, Reading 8,Lecture 8, Reading 9,Lecture 9, Reading 10,Lecture 10, -Quality Inspection Template Name,Nom du modèle d'inspection de la qualité, +Quality Inspection Template Name,Nom du modèle d'inspection de la qualité, Quick Stock Balance,Solde rapide des stocks, Available Quantity,quantité disponible, Distinct unit of an Item,Unité distincte d'un Article, @@ -8048,8 +8048,8 @@ Creation Document Type,Type de Document de Création, Creation Document No,N° du Document de Création, Creation Date,Date de Création, Creation Time,Date de Création, -Asset Details,Détails de l'actif, -Asset Status,Statut de l'actif, +Asset Details,Détails de l'actif, +Asset Status,Statut de l'actif, Delivery Document Type,Type de Document de Livraison, Delivery Document No,Numéro de Document de Livraison, Delivery Time,Heure de la Livraison, @@ -8064,13 +8064,13 @@ Out of AMC,Sur AMC, Warranty Period (Days),Période de Garantie (Jours), Serial No Details,Détails du N° de Série, MAT-STE-.YYYY.-,MAT-STE-.YYYY.-, -Stock Entry Type,Type d'entrée de stock, +Stock Entry Type,Type d'entrée de stock, Stock Entry (Outward GIT),Entrée de stock (GIT sortant), Material Consumption for Manufacture,Consommation de matériaux pour la production, Repack,Ré-emballer, Send to Subcontractor,Envoyer au sous-traitant, -Send to Warehouse,Envoyer à l'entrepôt, -Receive at Warehouse,Recevez à l'entrepôt, +Send to Warehouse,Envoyer à l'entrepôt, +Receive at Warehouse,Recevez à l'entrepôt, Delivery Note No,Bon de Livraison N°, Sales Invoice No,N° de la Facture de Vente, Purchase Receipt No,N° du Reçu d'Achat, @@ -8080,9 +8080,9 @@ For Quantity,Pour la Quantité, As per Stock UOM,Selon UDM du Stock, Including items for sub assemblies,Incluant les articles pour des sous-ensembles, Default Source Warehouse,Entrepôt Source par Défaut, -Source Warehouse Address,Adresse de l'entrepôt source, +Source Warehouse Address,Adresse de l'entrepôt source, Default Target Warehouse,Entrepôt Cible par Défaut, -Target Warehouse Address,Adresse de l'entrepôt cible, +Target Warehouse Address,Adresse de l'entrepôt cible, Update Rate and Availability,Mettre à Jour le Prix et la Disponibilité, Total Incoming Value,Valeur Entrante Totale, Total Outgoing Value,Valeur Sortante Totale, @@ -8102,7 +8102,7 @@ Subcontracted Item,Article sous-traité, Against Stock Entry,Contre entrée de stock, Stock Entry Child,Entrée de stock enfant, PO Supplied Item,PO article fourni, -Reference Purchase Receipt,Reçu d'achat de référence, +Reference Purchase Receipt,Reçu d'achat de référence, Stock Ledger Entry,Écriture du Livre d'Inventaire, Outgoing Rate,Taux Sortant, Actual Qty After Transaction,Qté Réelle Après Transaction, @@ -8126,9 +8126,9 @@ Default Stock UOM,UDM par Défaut des Articles, Sample Retention Warehouse,Entrepôt de stockage des échantillons, Default Valuation Method,Méthode de Valorisation par Défaut, Percentage you are allowed to receive or deliver more against the quantity ordered. For example: If you have ordered 100 units. and your Allowance is 10% then you are allowed to receive 110 units.,Pourcentage que vous êtes autorisé à recevoir ou à livrer en plus de la quantité commandée. Par exemple : Si vous avez commandé 100 unités et que votre allocation est de 10% alors que vous êtes autorisé à recevoir 110 unités., -Action if Quality inspection is not submitted,Action si l'inspection qualité n'est pas soumise, +Action if Quality inspection is not submitted,Action si l'inspection qualité n'est pas soumise, Show Barcode Field,Afficher Champ Code Barre, -Convert Item Description to Clean HTML,Convertir la description de l'élément pour nettoyer le code HTML, +Convert Item Description to Clean HTML,Convertir la description de l'élément pour nettoyer le code HTML, Auto insert Price List rate if missing,Insertion automatique du taux de la Liste de Prix si manquante, Allow Negative Stock,Autoriser un Stock Négatif, Automatically Set Serial Nos based on FIFO,Régler Automatiquement les Nos de Série basés sur FIFO, @@ -8162,7 +8162,7 @@ Service Level Agreement Fulfilled,Contrat de niveau de service rempli, Ongoing,En cours, Resolution By,Résolution de, Resolution By Variance,Résolution par variance, -Service Level Agreement Creation,Création d'un contrat de niveau de service, +Service Level Agreement Creation,Création d'un contrat de niveau de service, Mins to First Response,Minutes avant la Première Réponse, First Responded On,Première Réponse Le, Resolution Details,Détails de la Résolution, @@ -8171,7 +8171,7 @@ Opening Time,Horaire d'Ouverture, Resolution Date,Date de Résolution, Via Customer Portal,Via le portail client, Support Team,Équipe de Support, -Issue Priority,Priorité d'émission, +Issue Priority,Priorité d'émission, Service Day,Jour de service, Workday,Journée de travail, Holiday List (ignored during SLA calculation),Liste de jours fériés (ignorée lors du calcul du contrat de niveau de service), @@ -8182,7 +8182,7 @@ Support Hours,Heures de Support, Support and Resolution,Support et résolution, Default Service Level Agreement,Contrat de niveau de service par défaut, Entity,Entité, -Agreement Details,Détails de l'accord, +Agreement Details,Détails de l'accord, Response and Resolution Time,Temps de réponse et de résolution, Service Level Priority,Priorité de niveau de service, Response Time,Temps de réponse, @@ -8202,11 +8202,11 @@ Post Description Key,Clé de description du message, Link Options,Options du lien, Source DocType,DocType source, Result Title Field,Champ du titre du résultat, -Result Preview Field,Champ d'aperçu du résultat, +Result Preview Field,Champ d'aperçu du résultat, Result Route Field,Champ du lien du résultat, Service Level Agreements,Accords de Niveau de Service, Track Service Level Agreement,Suivi du contrat de niveau de service, -Allow Resetting Service Level Agreement,Autoriser la réinitialisation de l'accord de niveau de service, +Allow Resetting Service Level Agreement,Autoriser la réinitialisation de l'accord de niveau de service, Close Issue After Days,Nbre de jours avant de fermer le ticket, Auto close Issue after 7 days,Fermer automatiquement le ticket après 7 jours, Support Portal,Portail du support, @@ -8263,12 +8263,12 @@ Course wise Assessment Report,Rapport d'Évaluation par Cours, Customer Acquisition and Loyalty,Acquisition et Fidélisation des Clients, Customer Credit Balance,Solde de Crédit des Clients, Customer Ledger Summary,Récapitulatif client, -Customer-wise Item Price,Prix de l'article par client, +Customer-wise Item Price,Prix de l'article par client, Customers Without Any Sales Transactions,Clients sans transactions de vente, Daily Timesheet Summary,Récapitulatif Quotidien des Feuilles de Présence, Daily Work Summary Replies,Réponses au récapitulatif de travail quotidien, DATEV,DATEV, -Delayed Item Report,Rapport d'élément retardé, +Delayed Item Report,Rapport d'élément retardé, Delayed Order Report,Rapport de commande retardé, Delivered Items To Be Billed,Articles Livrés à Facturer, Delivery Note Trends,Tendance des Bordereaux de Livraisons, @@ -8305,7 +8305,7 @@ Item Price Stock,Stock et prix de l'article, Item Prices,Prix des Articles, Item Shortage Report,Rapport de Rupture de Stock d'Article, Project Quantity,Quantité de Projet, -Item Variant Details,Détails de la variante de l'article, +Item Variant Details,Détails de la variante de l'article, Item-wise Price List Rate,Taux de la Liste des Prix par Article, Item-wise Purchase History,Historique d'Achats par Article, Item-wise Purchase Register,Registre des Achats par Article, @@ -8395,11 +8395,11 @@ Supplier-Wise Sales Analytics,Analyse des Ventes par Fournisseur, Support Hour Distribution,Répartition des Heures de Support, TDS Computation Summary,Résumé des calculs TDS, TDS Payable Monthly,TDS Payable Monthly, -Territory Target Variance Based On Item Group,Écart de cible de territoire basé sur un groupe d'articles, +Territory Target Variance Based On Item Group,Écart de cible de territoire basé sur un groupe d'articles, Territory-wise Sales,Ventes par territoire, Total Stock Summary,Récapitulatif de l'Inventaire Total, Trial Balance,Balance Générale, -Trial Balance (Simple),Balance d'essai (simple), +Trial Balance (Simple),Balance d'essai (simple), Trial Balance for Party,Balance Auxiliaire, Unpaid Expense Claim,Note de Frais Impayée, Warehouse wise Item Balance Age and Value,Balance des articles par entrepôt, diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index ea96503dff..c8e3330908 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -120,6 +120,15 @@ class TransactionBase(StatusUpdater): def validate_rate_with_reference_doc(self, ref_details): + buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"] + + if self.doctype in buying_doctypes: + to_disable = "Maintain same rate throughout Purchase cycle" + settings_page = "Buying Settings" + else: + to_disable = "Maintain same rate throughout Sales cycle" + settings_page = "Selling Settings" + for ref_dt, ref_dn_field, ref_link_field in ref_details: for d in self.get("items"): if d.get(ref_link_field): @@ -129,8 +138,8 @@ class TransactionBase(StatusUpdater): frappe.msgprint(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4}) ") .format(d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate)) frappe.throw(_("To allow different rates, disable the {0} checkbox in {1}.") - .format(frappe.bold(_("Maintain Same Rate Throughout Sales Cycle")), - get_link_to_form("Selling Settings", "Selling Settings", frappe.bold("Selling Settings")))) + .format(frappe.bold(_(to_disable)), + get_link_to_form(settings_page, settings_page, frappe.bold(settings_page)))) def get_link_filters(self, for_doctype): if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype): @@ -161,19 +170,19 @@ class TransactionBase(StatusUpdater): if not (self.get("update_stock") or self.get("is_pos")): return - fiscal_year = get_fiscal_year(self.get('posting_date'), as_dict=True).name + for item in self.get('items'): + last_transaction_time = frappe.db.sql(""" + select MAX(timestamp(posting_date, posting_time)) as posting_time + from `tabStock Ledger Entry` + where docstatus = 1 and item_code = %s """, (item.item_code))[0][0] - last_transaction_time = frappe.db.sql(""" - select MAX(timestamp(posting_date, posting_time)) as posting_time - from `tabStock Ledger Entry` - where docstatus = 1 and fiscal_year = %s""", (fiscal_year))[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): - frappe.throw(_("""Posting timestamp of current transaction - must be after last Stock transaction's timestamp which is {0}""").format(frappe.bold(last_transaction_time)), - title=_("Backdated Stock Entry")) + if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time): + msg = _("Last Stock Transaction for item {0} was on {1}.").format(frappe.bold(item.item_code), frappe.bold(last_transaction_time)) + msg += "

    " + _("Stock Transactions for Item {0} cannot be posted before this time.").format(frappe.bold(item.item_code)) + msg += "

    " + _("Please remove this item and try to submit again or update the posting time.") + frappe.throw(msg, title=_("Backdated Stock Entry")) def delete_events(ref_type, ref_name): events = frappe.db.sql_list(""" SELECT diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html index f09021412b..0126b59c64 100644 --- a/erpnext/www/all-products/index.html +++ b/erpnext/www/all-products/index.html @@ -11,7 +11,7 @@
    diff --git a/erpnext/www/lms/content.html b/erpnext/www/lms/content.html index cdc71412c4..dc9b6d80fb 100644 --- a/erpnext/www/lms/content.html +++ b/erpnext/www/lms/content.html @@ -59,7 +59,7 @@ {% macro title() %}
    @@ -69,15 +69,15 @@ {% macro navigation() %} {% if previous %} - Previous + {{_('Previous')}} {% else %} - Back to Course + {{ _('Back to Course') }} {% endif %} {% if next %} - + {% else %} - + {% endif %} {% endmacro %} @@ -86,7 +86,7 @@ {{ title() }}
    {% if content.duration %} - {{ content.duration }} Mins + {{ content.duration }} {{_('Mins')}} {% endif %} {% if content.publish_date and content.duration%} @@ -94,7 +94,7 @@ {% endif %} {% if content.publish_date %} - Published on {{ content.publish_date.strftime('%d, %b %Y') }} + {{_('Published on')}} {{ content.publish_date.strftime('%d, %b %Y') }} {% endif %}
    @@ -109,13 +109,13 @@ {{ title() }}
    {% if content.author or content.publish_date %} - Published + {{_('Published')}} {% endif %} {% if content.author %} - by {{ content.author }} + {{_('by')}} {{ content.author }} {% endif %} {% if content.publish_date %} - on {{ content.publish_date.strftime('%d, %b %Y') }} + {{_('on')}} {{ content.publish_date.strftime('%d, %b %Y') }} {% endif %}
    @@ -205,4 +205,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/www/lms/course.html b/erpnext/www/lms/course.html index f2fd9363e8..0d70ed5cef 100644 --- a/erpnext/www/lms/course.html +++ b/erpnext/www/lms/course.html @@ -72,11 +72,11 @@ {% if has_access %} diff --git a/erpnext/www/lms/index.html b/erpnext/www/lms/index.html index ffb4419f36..7ce3521273 100644 --- a/erpnext/www/lms/index.html +++ b/erpnext/www/lms/index.html @@ -45,7 +45,7 @@

    {{ education_settings.description }}

    {% if frappe.session.user == 'Guest' %} - Sign Up + {{_('Sign Up')}} {% endif %}

    diff --git a/erpnext/www/lms/macros/card.html b/erpnext/www/lms/macros/card.html index 076061d41b..dc8fc5c72c 100644 --- a/erpnext/www/lms/macros/card.html +++ b/erpnext/www/lms/macros/card.html @@ -15,8 +15,8 @@ {% if has_access or program.intro_video%} {% endif %} diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html index 66bb861c46..94f239eb8e 100644 --- a/erpnext/www/lms/macros/hero.html +++ b/erpnext/www/lms/macros/hero.html @@ -2,16 +2,16 @@

    {{ title }}

    {{ description or ''}}

    {% if frappe.session.user == 'Guest' %} - Sign Up + {{_('Sign Up')}} {% elif not has_access %} - + {% endif %}

    @@ -28,7 +28,7 @@ let btn = document.getElementById('enroll'); btn.disbaled = true; - btn.innerText = 'Enrolling...' + btn.innerText = __('Enrolling...') let opts = { method: 'erpnext.education.utils.enroll_in_program', @@ -44,7 +44,7 @@ window.location.reload() } }) - success_dialog.set_message('You have successfully enrolled for the program '); + success_dialog.set_message(__('You have successfully enrolled for the program ')); success_dialog.$message.show() success_dialog.show(); btn.disbaled = false; diff --git a/erpnext/www/lms/profile.html b/erpnext/www/lms/profile.html index 9508daedb7..5755dfe6d8 100644 --- a/erpnext/www/lms/profile.html +++ b/erpnext/www/lms/profile.html @@ -30,7 +30,7 @@ @@ -43,11 +43,11 @@

    {{ student.first_name }} {{ student.last_name or '' }}

    @@ -61,4 +61,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/www/lms/program.html b/erpnext/www/lms/program.html index 271b7813bb..7ad618630a 100644 --- a/erpnext/www/lms/program.html +++ b/erpnext/www/lms/program.html @@ -55,11 +55,11 @@ {% if has_access and progress[course.name] %} {% endif %} diff --git a/erpnext/www/lms/topic.html b/erpnext/www/lms/topic.html index 1f0d187664..cd24616cd4 100644 --- a/erpnext/www/lms/topic.html +++ b/erpnext/www/lms/topic.html @@ -23,13 +23,13 @@ {% if has_access %} diff --git a/erpnext/www/support/__init__.py b/erpnext/www/support/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/www/support/index.html b/erpnext/www/support/index.html new file mode 100644 index 0000000000..93da503dbb --- /dev/null +++ b/erpnext/www/support/index.html @@ -0,0 +1,58 @@ +{% extends "templates/web.html" %} + +{% block content %} +
    +
    +
    +

    {{ greeting_title or _("We're here to help!") }}

    + {% if greeting_subtitle %} +

    {{ greeting_subtitle }}

    + {% endif %} +
    +
    +
    + +{% if favorite_article_list %} +
    +
    +

    {{ _("Frequently Read Articles") }}

    +
    + {% for favorite_article in favorite_article_list %} +
    +
    +
    +
    + {{ favorite_article['category'] }}
    +

    {{ favorite_article['title'] }}

    +

    {{ favorite_article['description'] }}

    +
    + +
    +
    + {% endfor %} +
    +
    +
    +{% endif %} + +{% if help_article_list %} +
    +
    +

    {{ _("Help Articles") }}

    +
    + {% for item in help_article_list %} +
    +
    {{ item['category'].name }}
    +
    + {% for article in item['articles'] %} + {{ article.title }} + {% endfor %} +
    +
    + {% endfor %} +
    +
    +
    +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/erpnext/www/support/index.py b/erpnext/www/support/index.py new file mode 100644 index 0000000000..5d267430c1 --- /dev/null +++ b/erpnext/www/support/index.py @@ -0,0 +1,74 @@ +from __future__ import unicode_literals +import frappe + +def get_context(context): + context.no_cache = 1 + context.align_greeting = '' + setting = frappe.get_doc("Support Settings") + + context.greeting_title = setting.greeting_title + context.greeting_subtitle = setting.greeting_subtitle + + # Support content + favorite_articles = get_favorite_articles_by_page_view() + if len(favorite_articles) < 6: + name_list = [] + if favorite_articles: + for article in favorite_articles: + name_list.append(article.name) + for record in (frappe.get_all("Help Article", + fields=["title", "content", "route", "category"], + filters={"name": ['not in', tuple(name_list)], "published": 1}, + order_by="creation desc", limit=(6-len(favorite_articles)))): + favorite_articles.append(record) + + context.favorite_article_list = get_favorite_articles(favorite_articles) + context.help_article_list = get_help_article_list() + +def get_favorite_articles_by_page_view(): + return frappe.db.sql( + """ + SELECT + t1.name as name, + t1.title as title, + t1.content as content, + t1.route as route, + t1.category as category, + count(t1.route) as count + FROM `tabHelp Article` AS t1 + INNER JOIN + `tabWeb Page View` AS t2 + ON t1.route = t2.path + WHERE t1.published = 1 + GROUP BY route + ORDER BY count DESC + LIMIT 6; + """, as_dict=True) + +def get_favorite_articles(favorite_articles): + favorite_article_list=[] + for article in favorite_articles: + description = frappe.utils.strip_html(article.content) + if len(description) > 120: + description = description[:120] + '...' + favorite_article_dict = { + 'title': article.title, + 'description': description, + 'route': article.route, + 'category': article.category, + } + favorite_article_list.append(favorite_article_dict) + return favorite_article_list + +def get_help_article_list(): + help_article_list=[] + category_list = frappe.get_all("Help Category", fields="name") + for category in category_list: + help_articles = frappe.get_all("Help Article", fields="*", filters={"category": category.name, "published": 1}, order_by="modified desc", limit=5) + if help_articles: + help_aricles_per_caetgory = { + 'category': category, + 'articles': help_articles, + } + help_article_list.append(help_aricles_per_caetgory) + return help_article_list \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9da537e493..912d61f7a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,12 @@ braintree==3.57.1 frappe gocardless-pro==1.11.0 googlemaps==3.1.1 -pandas==0.24.2 +pandas==1.0.5 plaid-python==3.4.0 +pycountry==19.8.18 PyGithub==1.44.1 python-stdnum==1.12 +taxjar==1.9.0 +tweepy==3.8.0 Unidecode==1.1.1 WooCommerce==2.1.1 -tweepy==3.8.0 \ No newline at end of file