Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into backport-asset-repair-refactor

This commit is contained in:
GangaManoj 2021-08-06 18:12:24 +05:30
commit fcbd79b7d3
156 changed files with 3002 additions and 3046 deletions

View File

@ -98,8 +98,6 @@ rules:
languages: [python] languages: [python]
severity: WARNING severity: WARNING
paths: paths:
exclude:
- test_*.py
include: include:
- "*/**/doctype/*" - "*/**/doctype/*"

View File

@ -8,18 +8,3 @@ rules:
dynamic content. Avoid it or use safe_eval(). dynamic content. Avoid it or use safe_eval().
languages: [python] languages: [python]
severity: ERROR severity: ERROR
- id: frappe-sqli-format-strings
patterns:
- pattern-inside: |
@frappe.whitelist()
def $FUNC(...):
...
- pattern-either:
- pattern: frappe.db.sql("..." % ...)
- pattern: frappe.db.sql(f"...", ...)
- pattern: frappe.db.sql("...".format(...), ...)
message: |
Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
languages: [python]
severity: WARNING

View File

@ -1,16 +1,25 @@
name: Backport name: Backport
on: on:
pull_request: pull_request_target:
types: types:
- closed - closed
- labeled - labeled
jobs: jobs:
backport: main:
runs-on: ubuntu-18.04 runs-on: ubuntu-latest
name: Backport
steps: steps:
- name: Backport - name: Checkout Actions
uses: tibdex/backport@v1 uses: actions/checkout@v2
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} repository: "frappe/backport"
path: ./actions
ref: develop
- name: Install Actions
run: npm install --production --prefix ./actions
- name: Run backport
uses: ./actions/backport
with:
token: ${{secrets.BACKPORT_BOT_TOKEN}}
labelsToAdd: "backport"
title: "{{originalTitle}}"

View File

@ -1,34 +1,18 @@
name: Semgrep name: Semgrep
on: on:
pull_request: pull_request: { }
branches:
- develop
- version-13-hotfix
- version-13-pre-release
jobs: jobs:
semgrep: semgrep:
name: Frappe Linter name: Frappe Linter
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Setup python3 - uses: returntocorp/semgrep-action@v1
uses: actions/setup-python@v2 env:
with: SEMGREP_TIMEOUT: 120
python-version: 3.8 with:
config: >-
- name: Setup semgrep r/python.lang.correctness
run: | .github/helper/semgrep_rules
python -m pip install -q semgrep
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
- name: Semgrep errors
run: |
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
semgrep --config="r/python.lang.correctness" --quiet --error $files
- name: Semgrep warnings
run: |
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files

View File

@ -3,16 +3,33 @@
# These owners will be the default owners for everything in # These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, # the repo. Unless a later match takes precedence,
manufacturing/ @rohitwaghchaure @marination erpnext/accounts/ @nextchamp-saqib @deepeshgarg007
accounts/ @deepeshgarg007 @nextchamp-saqib erpnext/assets/ @nextchamp-saqib @deepeshgarg007
loan_management/ @deepeshgarg007 @rohitwaghchaure erpnext/erpnext_integrations/ @nextchamp-saqib
pos* @nextchamp-saqib @rohitwaghchaure erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
assets/ @nextchamp-saqib @deepeshgarg007 erpnext/regional @nextchamp-saqib @deepeshgarg007
stock/ @marination @rohitwaghchaure erpnext/selling @nextchamp-saqib @deepeshgarg007
buying/ @marination @deepeshgarg007 erpnext/support/ @nextchamp-saqib @deepeshgarg007
hr/ @Anurag810 @rohitwaghchaure pos* @nextchamp-saqib
projects/ @hrwX @nextchamp-saqib
support/ @hrwX @marination erpnext/buying/ @marination @rohitwaghchaure @ankush
healthcare/ @ruchamahabal @marination erpnext/e_commerce/ @marination
erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib erpnext/maintenance/ @marination @rohitwaghchaure
requirements.txt @gavindsouza erpnext/manufacturing/ @marination @rohitwaghchaure @ankush
erpnext/portal/ @marination
erpnext/quality_management/ @marination @rohitwaghchaure
erpnext/shopping_cart/ @marination
erpnext/stock/ @marination @rohitwaghchaure @ankush
erpnext/crm/ @ruchamahabal @pateljannat
erpnext/education/ @ruchamahabal @pateljannat
erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
erpnext/hr/ @ruchamahabal @pateljannat
erpnext/non_profit/ @ruchamahabal
erpnext/payroll @ruchamahabal @pateljannat
erpnext/projects/ @ruchamahabal @pateljannat
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
.github/ @surajshetty3416 @ankush
requirements.txt @gavindsouza

View File

@ -230,7 +230,7 @@ class Account(NestedSet):
if self.check_gle_exists(): if self.check_gle_exists():
throw(_("Account with existing transaction can not be converted to group.")) throw(_("Account with existing transaction can not be converted to group."))
elif self.account_type and not self.flags.exclude_account_type_check: elif self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected.")) throw(_("Cannot convert to Group because Account Type is selected."))
else: else:
self.is_group = 1 self.is_group = 1
self.save() self.save()

View File

@ -249,7 +249,7 @@ class TestBudget(unittest.TestCase):
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None): def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
if budget_against_field == "project": if budget_against_field == "project":
budget_against = "_Test Project" budget_against = frappe.db.get_value("Project", {"project_name": "_Test Project"})
else: else:
budget_against = budget_against_CC or "_Test Cost Center - _TC" budget_against = budget_against_CC or "_Test Cost Center - _TC"
@ -275,7 +275,7 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
elif budget_against_field == "project": elif budget_against_field == "project":
make_journal_entry("_Test Account Cost for Goods Sold - _TC", make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date=nowdate()) "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate())
def make_budget(**args): def make_budget(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -13,7 +13,8 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file
class ChartofAccountsImporter(Document): class ChartofAccountsImporter(Document):
pass def validate(self):
validate_accounts(self.import_file)
@frappe.whitelist() @frappe.whitelist()
def validate_company(company): def validate_company(company):
@ -301,28 +302,27 @@ def validate_accounts(file_name):
if account["parent_account"] and accounts_dict.get(account["parent_account"]): if account["parent_account"] and accounts_dict.get(account["parent_account"]):
accounts_dict[account["parent_account"]]["is_group"] = 1 accounts_dict[account["parent_account"]]["is_group"] = 1
message = validate_root(accounts_dict) validate_root(accounts_dict)
if message: return message
message = validate_account_types(accounts_dict) validate_account_types(accounts_dict)
if message: return message
return [True, len(accounts)] return [True, len(accounts)]
def validate_root(accounts): def validate_root(accounts):
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')] roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
if len(roots) < 4: if len(roots) < 4:
return _("Number of root accounts cannot be less than 4") frappe.throw(_("Number of root accounts cannot be less than 4"))
error_messages = [] error_messages = []
for account in roots: for account in roots:
if not account.get("root_type") and account.get("account_name"): if not account.get("root_type") and account.get("account_name"):
error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name"))) error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name")))
elif account.get("root_type") not in get_root_types() and account.get("account_name"): elif account.get("root_type") not in get_root_types() and account.get("account_name"):
error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name"))) error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
if error_messages: if error_messages:
return "<br>".join(error_messages) frappe.throw("<br>".join(error_messages))
def get_root_types(): def get_root_types():
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity') return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
@ -356,7 +356,7 @@ def validate_account_types(accounts):
missing = list(set(account_types_for_ledger) - set(account_types)) missing = list(set(account_types_for_ledger) - set(account_types))
if missing: if missing:
return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)) frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
account_types_for_group = ["Bank", "Cash", "Stock"] account_types_for_group = ["Bank", "Cash", "Stock"]
# fix logic bug # fix logic bug
@ -364,7 +364,7 @@ def validate_account_types(accounts):
missing = list(set(account_types_for_group) - set(account_groups)) missing = list(set(account_types_for_group) - set(account_groups))
if missing: if missing:
return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)) frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
def unset_existing_data(company): def unset_existing_data(company):
linked = frappe.db.sql('''select fieldname from tabDocField linked = frappe.db.sql('''select fieldname from tabDocField
@ -391,5 +391,5 @@ def set_default_accounts(company):
}) })
company.save() company.save()
install_country_fixtures(company.name) install_country_fixtures(company.name, company.country)
company.create_default_tax_template() company.create_default_tax_template()

View File

@ -25,7 +25,7 @@ class Dunning(AccountsController):
def validate_amount(self): def validate_amount(self):
amounts = calculate_interest_and_amount( amounts = calculate_interest_and_amount(
self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
if self.interest_amount != amounts.get('interest_amount'): if self.interest_amount != amounts.get('interest_amount'):
self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount')) self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount'))
if self.dunning_amount != amounts.get('dunning_amount'): if self.dunning_amount != amounts.get('dunning_amount'):
@ -91,13 +91,13 @@ def resolve_dunning(doc, state):
for dunning in dunnings: for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days): def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
interest_amount = 0 interest_amount = 0
grand_total = 0 grand_total = flt(outstanding_amount) + flt(dunning_fee)
if rate_of_interest: if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
interest_amount = (interest_per_year * cint(overdue_days)) / 365 interest_amount = (interest_per_year * cint(overdue_days)) / 365
grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) grand_total += flt(interest_amount)
dunning_amount = flt(interest_amount) + flt(dunning_fee) dunning_amount = flt(interest_amount) + flt(dunning_fee)
return { return {
'interest_amount': interest_amount, 'interest_amount': interest_amount,

View File

@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase):
@classmethod @classmethod
def setUpClass(self): def setUpClass(self):
create_dunning_type() create_dunning_type()
create_dunning_type_with_zero_interest_rate()
unlink_payment_on_cancel_of_invoice() unlink_payment_on_cancel_of_invoice()
@classmethod @classmethod
@ -25,11 +26,20 @@ class TestDunning(unittest.TestCase):
def test_dunning(self): def test_dunning(self):
dunning = create_dunning() dunning = create_dunning()
amounts = calculate_interest_and_amount( amounts = calculate_interest_and_amount(
dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44) self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44) self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44) self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
def test_dunning_with_zero_interest_rate(self):
dunning = create_dunning_with_zero_interest_rate()
amounts = calculate_interest_and_amount(
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
self.assertEqual(round(amounts.get('interest_amount'), 2), 0)
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20)
self.assertEqual(round(amounts.get('grand_total'), 2), 120)
def test_gl_entries(self): def test_gl_entries(self):
dunning = create_dunning() dunning = create_dunning()
dunning.submit() dunning.submit()
@ -83,6 +93,27 @@ def create_dunning():
dunning.save() dunning.save()
return dunning return dunning
def create_dunning_with_zero_interest_rate():
posting_date = add_days(today(), -20)
due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=posting_date, due_date=due_date, status='Overdue')
dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest')
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
dunning.outstanding_amount = sales_invoice.outstanding_amount
dunning.debit_to = sales_invoice.debit_to
dunning.currency = sales_invoice.currency
dunning.company = sales_invoice.company
dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date
dunning.dunning_type = 'First Notice with 0% Rate of Interest'
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
def create_dunning_type(): def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type") dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = 'First Notice' dunning_type.dunning_type = 'First Notice'
@ -98,3 +129,19 @@ def create_dunning_type():
} }
) )
dunning_type.save() dunning_type.save()
def create_dunning_type_with_zero_interest_rate():
dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = 'First Notice with 0% Rate of Interest'
dunning_type.start_day = 10
dunning_type.end_day = 20
dunning_type.dunning_fee = 20
dunning_type.rate_of_interest = 0
dunning_type.append(
"dunning_letter_text", {
'language': 'en',
'body_text': 'We have still not received payment for our invoice ',
'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.'
}
)
dunning_type.save()

View File

@ -27,6 +27,9 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date): if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries")) frappe.throw(_("Please select Company and Posting Date to getting entries"))
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
@frappe.whitelist() @frappe.whitelist()
def check_journal_entry_condition(self): def check_journal_entry_condition(self):
total_debit = frappe.db.get_value("Journal Entry Account", { total_debit = frappe.db.get_value("Journal Entry Account", {
@ -99,10 +102,12 @@ class ExchangeRateRevaluation(Document):
sum(debit) - sum(credit) as balance sum(debit) - sum(credit) as balance
from `tabGL Entry` from `tabGL Entry`
where account in (%s) where account in (%s)
group by account, party_type, party and posting_date <= %s
and is_cancelled = 0
group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit) having sum(debit) != sum(credit)
order by account order by account
""" % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1) """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1)
return account_details return account_details
@ -143,9 +148,9 @@ class ExchangeRateRevaluation(Document):
"party_type": d.get("party_type"), "party_type": d.get("party_type"),
"party": d.get("party"), "party": d.get("party"),
"account_currency": d.get("account_currency"), "account_currency": d.get("account_currency"),
"balance": d.get("balance_in_account_currency"), "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
dr_or_cr: abs(d.get("balance_in_account_currency")), dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate":d.get("new_exchange_rate"), "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
}) })
@ -154,9 +159,9 @@ class ExchangeRateRevaluation(Document):
"party_type": d.get("party_type"), "party_type": d.get("party_type"),
"party": d.get("party"), "party": d.get("party"),
"account_currency": d.get("account_currency"), "account_currency": d.get("account_currency"),
"balance": d.get("balance_in_account_currency"), "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
reverse_dr_or_cr: abs(d.get("balance_in_account_currency")), reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate": d.get("current_exchange_rate"), "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name "reference_name": self.name
}) })
@ -185,9 +190,9 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
account_details = {} account_details = {}
company_currency = erpnext.get_company_currency(company) company_currency = erpnext.get_company_currency(company)
balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False) balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False)
if balance: if balance:
balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party) balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0 current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate

View File

@ -7,6 +7,8 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
if(frm.doc.__islocal) { if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);

View File

@ -667,6 +667,7 @@
{ {
"fieldname": "base_paid_amount_after_tax", "fieldname": "base_paid_amount_after_tax",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1,
"label": "Paid Amount After Tax (Company Currency)", "label": "Paid Amount After Tax (Company Currency)",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
@ -693,21 +694,25 @@
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
"fieldname": "received_amount_after_tax", "fieldname": "received_amount_after_tax",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1,
"label": "Received Amount After Tax", "label": "Received Amount After Tax",
"options": "paid_to_account_currency" "options": "paid_to_account_currency",
"read_only": 1
}, },
{ {
"depends_on": "doc.received_amount", "depends_on": "doc.received_amount",
"fieldname": "base_received_amount_after_tax", "fieldname": "base_received_amount_after_tax",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1,
"label": "Received Amount After Tax (Company Currency)", "label": "Received Amount After Tax (Company Currency)",
"options": "Company:company:default_currency" "options": "Company:company:default_currency",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-06-22 20:37:06.154206", "modified": "2021-07-09 08:58:15.008761",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@ -183,6 +183,13 @@ class PaymentEntry(AccountsController):
d.reference_name, self.party_account_currency) d.reference_name, self.party_account_currency)
for field, value in iteritems(ref_details): for field, value in iteritems(ref_details):
if d.exchange_gain_loss:
# for cases where gain/loss is booked into invoice
# exchange_gain_loss is calculated from invoice & populated
# and row.exchange_rate is already set to payment entry's exchange rate
# refer -> `update_reference_in_payment_entry()` in utils.py
continue
if field == 'exchange_rate' or not d.get(field) or force: if field == 'exchange_rate' or not d.get(field) or force:
d.db_set(field, value) d.db_set(field, value)
@ -404,9 +411,15 @@ class PaymentEntry(AccountsController):
if not self.advance_tax_account: if not self.advance_tax_account:
frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction")) frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
reference_doclist = []
net_total = self.paid_amount net_total = self.paid_amount
included_in_paid_amount = 0
for reference in self.get("references"):
net_total_for_tds = 0
if reference.reference_doctype == 'Purchase Order':
net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total'))
if net_total_for_tds:
net_total = net_total_for_tds
# Adding args as purchase invoice to get TDS amount # Adding args as purchase invoice to get TDS amount
args = frappe._dict({ args = frappe._dict({
@ -423,7 +436,7 @@ class PaymentEntry(AccountsController):
return return
tax_withholding_details.update({ tax_withholding_details.update({
'included_in_paid_amount': included_in_paid_amount, 'add_deduct_tax': 'Add',
'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
}) })
@ -512,16 +525,19 @@ class PaymentEntry(AccountsController):
self.unallocated_amount = 0 self.unallocated_amount = 0
if self.party: if self.party:
total_deductions = sum(flt(d.amount) for d in self.get("deductions")) total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
if self.payment_type == "Receive" \ if self.payment_type == "Receive" \
and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \ and self.base_total_allocated_amount < self.base_received_amount + total_deductions \
and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate): and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate):
self.unallocated_amount = (self.received_amount_after_tax + total_deductions - self.unallocated_amount = (self.received_amount + total_deductions -
self.base_total_allocated_amount) / self.source_exchange_rate self.base_total_allocated_amount) / self.source_exchange_rate
self.unallocated_amount -= included_taxes
elif self.payment_type == "Pay" \ elif self.payment_type == "Pay" \
and self.base_total_allocated_amount < (self.base_paid_amount_after_tax - total_deductions) \ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \
and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate): and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate):
self.unallocated_amount = (self.base_paid_amount_after_tax - (total_deductions + self.unallocated_amount = (self.base_paid_amount - (total_deductions +
self.base_total_allocated_amount)) / self.target_exchange_rate self.base_total_allocated_amount)) / self.target_exchange_rate
self.unallocated_amount -= included_taxes
def set_difference_amount(self): def set_difference_amount(self):
base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate) base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate)
@ -530,17 +546,29 @@ class PaymentEntry(AccountsController):
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
if self.payment_type == "Receive": if self.payment_type == "Receive":
self.difference_amount = base_party_amount - self.base_received_amount_after_tax self.difference_amount = base_party_amount - self.base_received_amount
elif self.payment_type == "Pay": elif self.payment_type == "Pay":
self.difference_amount = self.base_paid_amount_after_tax - base_party_amount self.difference_amount = self.base_paid_amount - base_party_amount
else: else:
self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax) self.difference_amount = self.base_paid_amount - flt(self.base_received_amount)
total_deductions = sum(flt(d.amount) for d in self.get("deductions")) total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
self.difference_amount = flt(self.difference_amount - total_deductions, self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes,
self.precision("difference_amount")) self.precision("difference_amount"))
def get_included_taxes(self):
included_taxes = 0
for tax in self.get('taxes'):
if tax.included_in_paid_amount:
if tax.add_deduct_tax == 'Add':
included_taxes += tax.base_tax_amount
else:
included_taxes -= tax.base_tax_amount
return included_taxes
# Paid amount is auto allocated in the reference document by default. # Paid amount is auto allocated in the reference document by default.
# Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast # Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast
def clear_unallocated_reference_document_rows(self): def clear_unallocated_reference_document_rows(self):
@ -664,8 +692,8 @@ class PaymentEntry(AccountsController):
gl_entries.append(gle) gl_entries.append(gle)
if self.unallocated_amount: if self.unallocated_amount:
base_unallocated_amount = self.unallocated_amount * \ exchange_rate = self.get_exchange_rate()
(self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate) base_unallocated_amount = (self.unallocated_amount * exchange_rate)
gle = party_gl_dict.copy() gle = party_gl_dict.copy()
@ -683,8 +711,8 @@ class PaymentEntry(AccountsController):
"account": self.paid_from, "account": self.paid_from,
"account_currency": self.paid_from_account_currency, "account_currency": self.paid_from_account_currency,
"against": self.party if self.payment_type=="Pay" else self.paid_to, "against": self.party if self.payment_type=="Pay" else self.paid_to,
"credit_in_account_currency": self.paid_amount_after_tax, "credit_in_account_currency": self.paid_amount,
"credit": self.base_paid_amount_after_tax, "credit": self.base_paid_amount,
"cost_center": self.cost_center "cost_center": self.cost_center
}, item=self) }, item=self)
) )
@ -694,8 +722,8 @@ class PaymentEntry(AccountsController):
"account": self.paid_to, "account": self.paid_to,
"account_currency": self.paid_to_account_currency, "account_currency": self.paid_to_account_currency,
"against": self.party if self.payment_type=="Receive" else self.paid_from, "against": self.party if self.payment_type=="Receive" else self.paid_from,
"debit_in_account_currency": self.received_amount_after_tax, "debit_in_account_currency": self.received_amount,
"debit": self.base_received_amount_after_tax, "debit": self.base_received_amount,
"cost_center": self.cost_center "cost_center": self.cost_center
}, item=self) }, item=self)
) )
@ -708,35 +736,42 @@ class PaymentEntry(AccountsController):
if self.payment_type in ('Pay', 'Internal Transfer'): if self.payment_type in ('Pay', 'Internal Transfer'):
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
against = self.party or self.paid_from
elif self.payment_type == 'Receive': elif self.payment_type == 'Receive':
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
against = self.party or self.paid_to
payment_or_advance_account = self.get_party_account_for_taxes() payment_or_advance_account = self.get_party_account_for_taxes()
tax_amount = d.tax_amount
base_tax_amount = d.base_tax_amount
if self.advance_tax_account:
tax_amount = -1 * tax_amount
base_tax_amount = -1 * base_tax_amount
gl_entries.append( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({
"account": d.account_head, "account": d.account_head,
"against": self.party if self.payment_type=="Receive" else self.paid_from, "against": against,
dr_or_cr: d.base_tax_amount, dr_or_cr: tax_amount,
dr_or_cr + "_in_account_currency": d.base_tax_amount dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency==self.company_currency if account_currency==self.company_currency
else d.tax_amount, else d.tax_amount,
"cost_center": d.cost_center "cost_center": d.cost_center
}, account_currency, item=d)) }, account_currency, item=d))
#Intentionally use -1 to get net values in party account #Intentionally use -1 to get net values in party account
gl_entries.append( if not d.included_in_paid_amount or self.advance_tax_account:
self.get_gl_dict({ gl_entries.append(
"account": payment_or_advance_account, self.get_gl_dict({
"against": self.party if self.payment_type=="Receive" else self.paid_from, "account": payment_or_advance_account,
dr_or_cr: -1 * d.base_tax_amount, "against": against,
dr_or_cr + "_in_account_currency": -1*d.base_tax_amount dr_or_cr: -1 * tax_amount,
if account_currency==self.company_currency dr_or_cr + "_in_account_currency": -1 * base_tax_amount
else d.tax_amount, if account_currency==self.company_currency
"cost_center": self.cost_center, else d.tax_amount,
"party_type": self.party_type, "cost_center": self.cost_center,
"party": self.party }, account_currency, item=d))
}, account_currency, item=d))
def add_deductions_gl_entries(self, gl_entries): def add_deductions_gl_entries(self, gl_entries):
for d in self.get("deductions"): for d in self.get("deductions"):
@ -760,9 +795,9 @@ class PaymentEntry(AccountsController):
if self.advance_tax_account: if self.advance_tax_account:
return self.advance_tax_account return self.advance_tax_account
elif self.payment_type == 'Receive': elif self.payment_type == 'Receive':
return self.paid_from
elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_to return self.paid_to
elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_from
def update_advance_paid(self): def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party: if self.payment_type in ("Receive", "Pay") and self.party:
@ -806,10 +841,17 @@ class PaymentEntry(AccountsController):
if account_details: if account_details:
row.update(account_details) row.update(account_details)
if not row.get('amount'):
# if no difference amount
return
self.append('deductions', row) self.append('deductions', row)
self.set_unallocated_amount() self.set_unallocated_amount()
def get_exchange_rate(self):
return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate
def initialize_taxes(self): def initialize_taxes(self):
for tax in self.get("taxes"): for tax in self.get("taxes"):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
@ -1634,12 +1676,6 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
if dt == "Employee Advance": if dt == "Employee Advance":
paid_amount = received_amount * doc.get('exchange_rate', 1) paid_amount = received_amount * doc.get('exchange_rate', 1)
if dt == "Purchase Order" and doc.apply_tds:
if party_account_currency == bank.account_currency:
paid_amount = received_amount = doc.base_net_total
else:
paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1)
return paid_amount, received_amount return paid_amount, received_amount
def apply_early_payment_discount(paid_amount, received_amount, doc): def apply_early_payment_discount(paid_amount, received_amount, doc):

View File

@ -14,7 +14,8 @@
"total_amount", "total_amount",
"outstanding_amount", "outstanding_amount",
"allocated_amount", "allocated_amount",
"exchange_rate" "exchange_rate",
"exchange_gain_loss"
], ],
"fields": [ "fields": [
{ {
@ -90,12 +91,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Payment Term", "label": "Payment Term",
"options": "Payment Term" "options": "Payment Term"
},
{
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-10 11:25:47.144392", "modified": "2021-04-21 13:30:11.605388",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry Reference", "name": "Payment Entry Reference",

View File

@ -306,5 +306,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
} }
] ]
}) })
jv.flags.ignore_mandatory = True
jv.submit() jv.submit()

View File

@ -1545,6 +1545,7 @@
"fieldname": "consolidated_invoice", "fieldname": "consolidated_invoice",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Consolidated Sales Invoice", "label": "Consolidated Sales Invoice",
"no_copy": 1,
"options": "Sales Invoice", "options": "Sales Invoice",
"read_only": 1 "read_only": 1
} }
@ -1552,7 +1553,7 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-02-01 15:03:33.800707", "modified": "2021-07-29 13:37:20.636171",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@ -168,7 +168,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
frappe.throw(_("Invalid {0}").format(args.get(field))) frappe.throw(_("Invalid {0}").format(args.get(field)))
parent_groups = frappe.db.sql_list("""select name from `tab%s` parent_groups = frappe.db.sql_list("""select name from `tab%s`
where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
if parenttype in ["Customer Group", "Item Group", "Territory"]: if parenttype in ["Customer Group", "Item Group", "Territory"]:
parent_field = "parent_{0}".format(frappe.scrub(parenttype)) parent_field = "parent_{0}".format(frappe.scrub(parenttype))

View File

@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}, },
get_query_filters: { get_query_filters: {
docstatus: 1, docstatus: 1,
status: ["not in", ["Closed", "Completed"]], status: ["not in", ["Closed", "Completed", "Return Issued"]],
company: me.frm.doc.company, company: me.frm.doc.company,
is_return: 0 is_return: 0
} }

View File

@ -451,6 +451,7 @@ class PurchaseInvoice(BuyingController):
self.get_asset_gl_entry(gl_entries) self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries) self.make_tax_gl_entries(gl_entries)
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries) self.allocate_advance_taxes(gl_entries)

View File

@ -953,6 +953,120 @@ class TestPurchaseInvoice(unittest.TestCase):
acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save() acc_settings.save()
def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value(
"Accounts Settings", "Accounts Settings",
"unlink_payment_on_cancel_of_invoice")
frappe.db.set_value(
"Accounts Settings", "Accounts Settings",
"unlink_payment_on_cancel_of_invoice", 1)
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC")
pay = frappe.get_doc({
'doctype': 'Payment Entry',
'company': '_Test Company',
'payment_type': 'Pay',
'party_type': 'Supplier',
'party': '_Test Supplier USD',
'paid_to': '_Test Payable USD - _TC',
'paid_from': 'Cash - _TC',
'paid_amount': 70000,
'target_exchange_rate': 70,
'received_amount': 1000,
})
pay.insert()
pay.submit()
pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
conversion_rate=75, rate=500, do_not_save=1, qty=1)
pi.cost_center = "_Test Cost Center - _TC"
pi.advances = []
pi.append("advances", {
"reference_type": "Payment Entry",
"reference_name": pay.name,
"advance_amount": 1000,
"remarks": pay.remarks,
"allocated_amount": 500,
"ref_exchange_rate": 70
})
pi.save()
pi.submit()
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 37500.0],
["_Test Payable USD - _TC", -35000.0],
["Exchange Gain/Loss - _TC", -2500.0]
]
gl_entries = frappe.db.sql("""
select account, sum(debit - credit) as balance from `tabGL Entry`
where voucher_no=%s
group by account
order by account asc""", (pi.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance)
pi_2 = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
conversion_rate=73, rate=500, do_not_save=1, qty=1)
pi_2.cost_center = "_Test Cost Center - _TC"
pi_2.advances = []
pi_2.append("advances", {
"reference_type": "Payment Entry",
"reference_name": pay.name,
"advance_amount": 500,
"remarks": pay.remarks,
"allocated_amount": 500,
"ref_exchange_rate": 70
})
pi_2.save()
pi_2.submit()
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 36500.0],
["_Test Payable USD - _TC", -35000.0],
["Exchange Gain/Loss - _TC", -1500.0]
]
gl_entries = frappe.db.sql("""
select account, sum(debit - credit) as balance from `tabGL Entry`
where voucher_no=%s
group by account order by account asc""", (pi_2.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance)
expected_gle = [
["_Test Payable USD - _TC", 70000.0],
["Cash - _TC", -70000.0]
]
gl_entries = frappe.db.sql("""
select account, sum(debit - credit) as balance from `tabGL Entry`
where voucher_no=%s and is_cancelled=0
group by account order by account asc""", (pay.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.balance)
pi.reload()
pi.cancel()
pi_2.reload()
pi_2.cancel()
pay.reload()
pay.cancel()
frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled)
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
def test_purchase_invoice_advance_taxes(self): def test_purchase_invoice_advance_taxes(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry

View File

@ -1,235 +1,127 @@
{ {
"allow_copy": 0, "actions": [],
"allow_import": 0, "creation": "2013-03-08 15:36:46",
"allow_rename": 0, "doctype": "DocType",
"beta": 0, "document_type": "Document",
"creation": "2013-03-08 15:36:46", "editable_grid": 1,
"custom": 0, "engine": "InnoDB",
"docstatus": 0, "field_order": [
"doctype": "DocType", "reference_type",
"document_type": "Document", "reference_name",
"editable_grid": 1, "remarks",
"reference_row",
"col_break1",
"advance_amount",
"allocated_amount",
"exchange_gain_loss",
"ref_exchange_rate"
],
"fields": [ "fields": [
{ {
"allow_on_submit": 0, "fieldname": "reference_type",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "label": "Reference Type",
"columns": 0, "no_copy": 1,
"fieldname": "reference_type", "oldfieldname": "journal_voucher",
"fieldtype": "Link", "oldfieldtype": "Link",
"hidden": 0, "options": "DocType",
"ignore_user_permissions": 0, "print_width": "180px",
"ignore_xss_filter": 0, "read_only": 1,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Type",
"length": 0,
"no_copy": 1,
"oldfieldname": "journal_voucher",
"oldfieldtype": "Link",
"options": "DocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "180px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "180px" "width": "180px"
}, },
{ {
"allow_on_submit": 0, "columns": 3,
"bold": 0, "fieldname": "reference_name",
"collapsible": 0, "fieldtype": "Dynamic Link",
"columns": 3, "in_list_view": 1,
"fieldname": "reference_name", "label": "Reference Name",
"fieldtype": "Dynamic Link", "no_copy": 1,
"hidden": 0, "options": "reference_type",
"ignore_user_permissions": 0, "read_only": 1
"ignore_xss_filter": 0, },
"in_filter": 0,
"in_list_view": 1,
"label": "Reference Name",
"length": 0,
"no_copy": 1,
"options": "reference_type",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "columns": 3,
"bold": 0, "fieldname": "remarks",
"collapsible": 0, "fieldtype": "Text",
"columns": 3, "in_list_view": 1,
"fieldname": "remarks", "label": "Remarks",
"fieldtype": "Text", "no_copy": 1,
"hidden": 0, "oldfieldname": "remarks",
"ignore_user_permissions": 0, "oldfieldtype": "Small Text",
"ignore_xss_filter": 0, "print_width": "150px",
"in_filter": 0, "read_only": 1,
"in_list_view": 1,
"label": "Remarks",
"length": 0,
"no_copy": 1,
"oldfieldname": "remarks",
"oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "150px" "width": "150px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "reference_row",
"bold": 0, "fieldtype": "Data",
"collapsible": 0, "hidden": 1,
"columns": 0, "label": "Reference Row",
"fieldname": "reference_row", "no_copy": 1,
"fieldtype": "Data", "oldfieldname": "jv_detail_no",
"hidden": 1, "oldfieldtype": "Date",
"ignore_user_permissions": 0, "print_hide": 1,
"ignore_xss_filter": 0, "print_width": "80px",
"in_filter": 0, "read_only": 1,
"in_list_view": 0,
"label": "Reference Row",
"length": 0,
"no_copy": 1,
"oldfieldname": "jv_detail_no",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "80px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "80px" "width": "80px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "col_break1",
"bold": 0, "fieldtype": "Column Break"
"collapsible": 0, },
"columns": 0,
"fieldname": "col_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "columns": 2,
"bold": 0, "fieldname": "advance_amount",
"collapsible": 0, "fieldtype": "Currency",
"columns": 2, "in_list_view": 1,
"fieldname": "advance_amount", "label": "Advance Amount",
"fieldtype": "Currency", "no_copy": 1,
"hidden": 0, "oldfieldname": "advance_amount",
"ignore_user_permissions": 0, "oldfieldtype": "Currency",
"ignore_xss_filter": 0, "options": "party_account_currency",
"in_filter": 0, "print_width": "100px",
"in_list_view": 1, "read_only": 1,
"label": "Advance Amount",
"length": 0,
"no_copy": 1,
"oldfieldname": "advance_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "100px" "width": "100px"
}, },
{ {
"allow_on_submit": 0, "columns": 2,
"bold": 0, "fieldname": "allocated_amount",
"collapsible": 0, "fieldtype": "Currency",
"columns": 2, "in_list_view": 1,
"fieldname": "allocated_amount", "label": "Allocated Amount",
"fieldtype": "Currency", "no_copy": 1,
"hidden": 0, "oldfieldname": "allocated_amount",
"ignore_user_permissions": 0, "oldfieldtype": "Currency",
"ignore_xss_filter": 0, "options": "party_account_currency",
"in_filter": 0, "print_width": "100px",
"in_list_view": 1,
"label": "Allocated Amount",
"length": 0,
"no_copy": 1,
"oldfieldname": "allocated_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "100px",
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "100px" "width": "100px"
},
{
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "ref_exchange_rate",
"fieldtype": "Float",
"label": "Reference Exchange Rate",
"non_negative": 1,
"read_only": 1
} }
], ],
"hide_heading": 0, "idx": 1,
"hide_toolbar": 0, "index_web_pages_for_search": 1,
"idx": 1, "istable": 1,
"image_view": 0, "links": [],
"in_create": 0, "modified": "2021-04-20 16:26:53.820530",
"modified_by": "Administrator",
"is_submittable": 0, "module": "Accounts",
"issingle": 0, "name": "Purchase Invoice Advance",
"istable": 1, "owner": "Administrator",
"max_attachments": 0, "permissions": [],
"menu_index": 0, "quick_entry": 1,
"modified": "2016-08-26 02:30:54.407138", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC"
"module": "Accounts",
"name": "Purchase Invoice Advance",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_order": "DESC",
"track_seen": 0
} }

View File

@ -48,6 +48,8 @@
"shipping_address", "shipping_address",
"company_address", "company_address",
"company_address_display", "company_address_display",
"dispatch_address_name",
"dispatch_address",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@ -1966,6 +1968,21 @@
"fieldname": "disable_rounded_total", "fieldname": "disable_rounded_total",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable Rounded Total" "label": "Disable Rounded Total"
},
{
"allow_on_submit": 1,
"fieldname": "dispatch_address_name",
"fieldtype": "Link",
"label": "Dispatch Address Name",
"options": "Address",
"print_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "dispatch_address",
"fieldtype": "Small Text",
"label": "Dispatch Address",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -1978,7 +1995,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-05-20 22:48:33.988881", "modified": "2021-07-08 14:03:55.502522",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -840,6 +840,7 @@ class SalesInvoice(SellingController):
self.make_customer_gl_entry(gl_entries) self.make_customer_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries) self.make_tax_gl_entries(gl_entries)
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries) self.allocate_advance_taxes(gl_entries)

View File

@ -1908,6 +1908,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['sgstValue'], 5400)
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
self.assertEqual(data['billLists'][0]['fromStateCode'],27)
def test_einvoice_submission_without_irn(self): def test_einvoice_submission_without_irn(self):
# init # init
@ -2062,6 +2064,30 @@ def make_test_address_for_ewaybill():
address.save() address.save()
if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'):
address = frappe.get_doc({
"address_line1": "_Test Dispatch Address Line 1",
"address_title": "_Test Dispatch-Address for Eway bill",
"address_type": "Shipping",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 0,
"phone": "+910000000000",
"gstin": "07AAACC1206D1ZI",
"gst_state": "Delhi",
"gst_state_number": "07",
"pincode": "1100101"
}).insert()
address.append("links", {
"link_doctype": "Company",
"link_name": "_Test Company"
})
address.save()
def make_test_transporter_for_ewaybill(): def make_test_transporter_for_ewaybill():
if not frappe.db.exists('Supplier', '_Test Transporter'): if not frappe.db.exists('Supplier', '_Test Transporter'):
frappe.get_doc({ frappe.get_doc({
@ -2100,6 +2126,7 @@ def make_sales_invoice_for_ewaybill():
si.distance = 2000 si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing" si.company_address = "_Test Address for Eway bill-Billing"
si.customer_address = "_Test Customer-Address for Eway bill-Shipping" si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234" si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular" si.gst_category = "Registered Regular"
si.mode_of_transport = 'Road' si.mode_of_transport = 'Road'

View File

@ -1,235 +1,128 @@
{ {
"allow_copy": 0, "actions": [],
"allow_import": 0, "creation": "2013-02-22 01:27:41",
"allow_rename": 0, "doctype": "DocType",
"beta": 0, "document_type": "Document",
"creation": "2013-02-22 01:27:41", "editable_grid": 1,
"custom": 0, "engine": "InnoDB",
"docstatus": 0, "field_order": [
"doctype": "DocType", "reference_type",
"document_type": "Document", "reference_name",
"editable_grid": 1, "remarks",
"reference_row",
"col_break1",
"advance_amount",
"allocated_amount",
"exchange_gain_loss",
"ref_exchange_rate"
],
"fields": [ "fields": [
{ {
"allow_on_submit": 0, "fieldname": "reference_type",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "label": "Reference Type",
"columns": 0, "no_copy": 1,
"fieldname": "reference_type", "oldfieldname": "journal_voucher",
"fieldtype": "Link", "oldfieldtype": "Link",
"hidden": 0, "options": "DocType",
"ignore_user_permissions": 0, "print_width": "250px",
"ignore_xss_filter": 0, "read_only": 1,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Type",
"length": 0,
"no_copy": 1,
"oldfieldname": "journal_voucher",
"oldfieldtype": "Link",
"options": "DocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "250px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "250px" "width": "250px"
}, },
{ {
"allow_on_submit": 0, "columns": 3,
"bold": 0, "fieldname": "reference_name",
"collapsible": 0, "fieldtype": "Dynamic Link",
"columns": 3, "in_list_view": 1,
"fieldname": "reference_name", "label": "Reference Name",
"fieldtype": "Dynamic Link", "no_copy": 1,
"hidden": 0, "options": "reference_type",
"ignore_user_permissions": 0, "print_hide": 1,
"ignore_xss_filter": 0, "read_only": 1
"in_filter": 0, },
"in_list_view": 1,
"label": "Reference Name",
"length": 0,
"no_copy": 1,
"options": "reference_type",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "columns": 3,
"bold": 0, "fieldname": "remarks",
"collapsible": 0, "fieldtype": "Text",
"columns": 3, "in_list_view": 1,
"fieldname": "remarks", "label": "Remarks",
"fieldtype": "Text", "no_copy": 1,
"hidden": 0, "oldfieldname": "remarks",
"ignore_user_permissions": 0, "oldfieldtype": "Small Text",
"ignore_xss_filter": 0, "print_width": "150px",
"in_filter": 0, "read_only": 1,
"in_list_view": 1,
"label": "Remarks",
"length": 0,
"no_copy": 1,
"oldfieldname": "remarks",
"oldfieldtype": "Small Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "150px" "width": "150px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "reference_row",
"bold": 0, "fieldtype": "Data",
"collapsible": 0, "hidden": 1,
"columns": 0, "label": "Reference Row",
"fieldname": "reference_row", "no_copy": 1,
"fieldtype": "Data", "oldfieldname": "jv_detail_no",
"hidden": 1, "oldfieldtype": "Data",
"ignore_user_permissions": 0, "print_hide": 1,
"ignore_xss_filter": 0, "print_width": "120px",
"in_filter": 0, "read_only": 1,
"in_list_view": 0,
"label": "Reference Row",
"length": 0,
"no_copy": 1,
"oldfieldname": "jv_detail_no",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "120px" "width": "120px"
}, },
{ {
"allow_on_submit": 0, "fieldname": "col_break1",
"bold": 0, "fieldtype": "Column Break"
"collapsible": 0, },
"columns": 0,
"fieldname": "col_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "columns": 2,
"bold": 0, "fieldname": "advance_amount",
"collapsible": 0, "fieldtype": "Currency",
"columns": 2, "in_list_view": 1,
"fieldname": "advance_amount", "label": "Advance amount",
"fieldtype": "Currency", "no_copy": 1,
"hidden": 0, "oldfieldname": "advance_amount",
"ignore_user_permissions": 0, "oldfieldtype": "Currency",
"ignore_xss_filter": 0, "options": "party_account_currency",
"in_filter": 0, "print_width": "120px",
"in_list_view": 1, "read_only": 1,
"label": "Advance amount",
"length": 0,
"no_copy": 1,
"oldfieldname": "advance_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "120px" "width": "120px"
}, },
{ {
"allow_on_submit": 0, "columns": 2,
"bold": 0, "fieldname": "allocated_amount",
"collapsible": 0, "fieldtype": "Currency",
"columns": 2, "in_list_view": 1,
"fieldname": "allocated_amount", "label": "Allocated amount",
"fieldtype": "Currency", "no_copy": 1,
"hidden": 0, "oldfieldname": "allocated_amount",
"ignore_user_permissions": 0, "oldfieldtype": "Currency",
"ignore_xss_filter": 0, "options": "party_account_currency",
"in_filter": 0, "print_width": "120px",
"in_list_view": 1,
"label": "Allocated amount",
"length": 0,
"no_copy": 1,
"oldfieldname": "allocated_amount",
"oldfieldtype": "Currency",
"options": "party_account_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "120px" "width": "120px"
},
{
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "ref_exchange_rate",
"fieldtype": "Float",
"label": "Reference Exchange Rate",
"non_negative": 1,
"read_only": 1
} }
], ],
"hide_heading": 0, "idx": 1,
"hide_toolbar": 0, "index_web_pages_for_search": 1,
"idx": 1, "istable": 1,
"image_view": 0, "links": [],
"in_create": 0, "modified": "2021-06-04 20:25:49.832052",
"modified_by": "Administrator",
"is_submittable": 0, "module": "Accounts",
"issingle": 0, "name": "Sales Invoice Advance",
"istable": 1, "owner": "Administrator",
"max_attachments": 0, "permissions": [],
"menu_index": 0, "quick_entry": 1,
"modified": "2016-08-26 02:36:10.718057", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC"
"module": "Accounts",
"name": "Sales Invoice Advance",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_order": "DESC",
"track_seen": 0
} }

View File

@ -27,7 +27,8 @@
"base_tax_amount", "base_tax_amount",
"base_total", "base_total",
"base_tax_amount_after_discount_amount", "base_tax_amount_after_discount_amount",
"item_wise_tax_detail" "item_wise_tax_detail",
"dont_recompute_tax"
], ],
"fields": [ "fields": [
{ {
@ -200,13 +201,22 @@
"fieldname": "included_in_paid_amount", "fieldname": "included_in_paid_amount",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Considered In Paid Amount" "label": "Considered In Paid Amount"
},
{
"default": "0",
"fieldname": "dont_recompute_tax",
"fieldtype": "Check",
"hidden": 1,
"label": "Dont Recompute tax",
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-14 01:44:36.899147", "modified": "2021-07-27 12:40:59.051803",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Taxes and Charges", "name": "Sales Taxes and Charges",

View File

@ -1,263 +1,151 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "allow_import": 1,
"allow_import": 1, "allow_rename": 1,
"allow_rename": 1, "autoname": "Prompt",
"autoname": "Prompt", "creation": "2018-04-13 18:42:06.431683",
"beta": 0, "doctype": "DocType",
"creation": "2018-04-13 18:42:06.431683", "editable_grid": 1,
"custom": 0, "engine": "InnoDB",
"docstatus": 0, "field_order": [
"doctype": "DocType", "category_details_section",
"document_type": "", "category_name",
"editable_grid": 1, "round_off_tax_amount",
"engine": "InnoDB", "column_break_2",
"consider_party_ledger_amount",
"tax_on_excess_amount",
"section_break_8",
"rates",
"section_break_7",
"accounts"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "category_name", "fieldname": "category_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Category Name", "label": "Category Name",
"length": 0, "show_days": 1,
"no_copy": 0, "show_seconds": 1
"permlevel": 0, },
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_8", "fieldname": "section_break_8",
"fieldtype": "Section Break", "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": "Tax Withholding Rates", "label": "Tax Withholding Rates",
"length": 0, "show_days": 1,
"no_copy": 0, "show_seconds": 1
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "rates", "fieldname": "rates",
"fieldtype": "Table", "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": "Rates", "label": "Rates",
"length": 0,
"no_copy": 0,
"options": "Tax Withholding Rate", "options": "Tax Withholding Rate",
"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, "reqd": 1,
"search_index": 0, "show_days": 1,
"set_only_once": 0, "show_seconds": 1
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_7",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"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": "Account Details", "label": "Account Details",
"length": 0, "show_days": 1,
"no_copy": 0, "show_seconds": 1
"permlevel": 0, },
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "accounts",
"allow_in_quick_entry": 0, "fieldtype": "Table",
"allow_on_submit": 0, "label": "Accounts",
"bold": 0, "options": "Tax Withholding Account",
"collapsible": 0, "reqd": 1,
"columns": 0, "show_days": 1,
"fieldname": "accounts", "show_seconds": 1
"fieldtype": "Table", },
"hidden": 0, {
"ignore_user_permissions": 0, "fieldname": "category_details_section",
"ignore_xss_filter": 0, "fieldtype": "Section Break",
"in_filter": 0, "label": "Category Details",
"in_global_search": 0, "show_days": 1,
"in_list_view": 0, "show_seconds": 1
"in_standard_filter": 0, },
"label": "Accounts", {
"length": 0, "fieldname": "column_break_2",
"no_copy": 0, "fieldtype": "Column Break",
"options": "Tax Withholding Account", "show_days": 1,
"permlevel": 0, "show_seconds": 1
"precision": "", },
"print_hide": 0, {
"print_hide_if_no_value": 0, "default": "0",
"read_only": 0, "description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"remember_last_selected_value": 0, "fieldname": "consider_party_ledger_amount",
"report_hide": 0, "fieldtype": "Check",
"reqd": 1, "label": "Consider Entire Party Ledger Amount",
"search_index": 0, "show_days": 1,
"set_only_once": 0, "show_seconds": 1
"translatable": 0, },
"unique": 0 {
"default": "0",
"description": "Tax will be withheld only for amount exceeding the cumulative threshold",
"fieldname": "tax_on_excess_amount",
"fieldtype": "Check",
"label": "Only Deduct Tax On Excess Amount ",
"show_days": 1,
"show_seconds": 1
},
{
"description": "Checking this will round off the tax amount to the nearest integer",
"fieldname": "round_off_tax_amount",
"fieldtype": "Check",
"label": "Round Off Tax Amount",
"show_days": 1,
"show_seconds": 1
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2021-07-27 21:47:34.396071",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Accounts",
"in_create": 0, "name": "Tax Withholding Category",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-17 22:53:26.193179",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "System Manager",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "Accounts Manager",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "Accounts User",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "DESC",
"read_only_onload": 0, "track_changes": 1
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, getdate from frappe.utils import flt, getdate, cint
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document): class TaxWithholdingCategory(Document):
@ -86,7 +86,10 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
"rate": tax_rate_detail.tax_withholding_rate, "rate": tax_rate_detail.tax_withholding_rate,
"threshold": tax_rate_detail.single_threshold, "threshold": tax_rate_detail.single_threshold,
"cumulative_threshold": tax_rate_detail.cumulative_threshold, "cumulative_threshold": tax_rate_detail.cumulative_threshold,
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
"consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount,
"tax_on_excess_amount": tax_withholding.tax_on_excess_amount,
"round_off_tax_amount": tax_withholding.round_off_tax_amount
}) })
def get_tax_withholding_rates(tax_withholding, fiscal_year): def get_tax_withholding_rates(tax_withholding, fiscal_year):
@ -145,6 +148,7 @@ def get_lower_deduction_certificate(fiscal_year, pan_no):
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None): def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
fiscal_year = fiscal_year_details[0] fiscal_year = fiscal_year_details[0]
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type) vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type) advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers taxable_vouchers = vouchers + advance_vouchers
@ -235,10 +239,18 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
tds_amount = 0 tds_amount = 0
invoice_filters = {
'name': ('in', vouchers),
'docstatus': 1
}
supp_credit_amt = frappe.db.get_value('Purchase Invoice', { field = 'sum(net_total)'
'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1
}, 'sum(net_total)') or 0.0 if not cint(tax_details.consider_party_ledger_amount):
invoice_filters.update({'apply_tds': 1})
field = 'sum(grand_total)'
supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0
supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', {
'parent': ('in', vouchers), 'docstatus': 1, 'parent': ('in', vouchers), 'docstatus': 1,
@ -255,6 +267,13 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
cumulative_threshold = tax_details.get('cumulative_threshold', 0) cumulative_threshold = tax_details.get('cumulative_threshold', 0)
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0
net_total += inv.net_total
supp_credit_amt = net_total - cumulative_threshold
if ldc and is_valid_certificate( if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto, ldc.valid_from, ldc.valid_upto,
inv.get('posting_date') or inv.get('transaction_date'), tax_deducted, inv.get('posting_date') or inv.get('transaction_date'), tax_deducted,
@ -263,6 +282,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
else: else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0 tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
if cint(tax_details.round_off_tax_amount):
tds_amount = round(tds_amount)
return tds_amount return tds_amount

View File

@ -87,6 +87,31 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices: for d in invoices:
d.cancel() d.cancel()
def test_tax_withholding_category_checks(self):
invoices = []
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
# First Invoice with no tds check
pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True)
pi.apply_tds = 0
pi.save()
pi.submit()
invoices.append(pi)
# Second Invoice will apply TDS checked
pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000)
pi1.submit()
invoices.append(pi1)
# Cumulative threshold is 30000
# Threshold calculation should be on both the invoices
# TDS should be applied only on 1000
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
for d in invoices:
d.cancel()
def test_cumulative_threshold_tcs(self): def test_cumulative_threshold_tcs(self):
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
invoices = [] invoices = []
@ -195,7 +220,7 @@ def create_sales_invoice(**args):
def create_records(): def create_records():
# create a new suppliers # create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']: for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']:
if frappe.db.exists('Supplier', name): if frappe.db.exists('Supplier', name):
continue continue
@ -311,3 +336,23 @@ def create_tax_with_holding_category():
'account': 'TDS - _TC' 'account': 'TDS - _TC'
}] }]
}).insert() }).insert()
if not frappe.db.exists("Tax Withholding Category", "New TDS Category"):
frappe.get_doc({
"doctype": "Tax Withholding Category",
"name": "New TDS Category",
"category_name": "New TDS Category",
"round_off_tax_amount": 1,
"consider_party_ledger_amount": 1,
"tax_on_excess_amount": 1,
"rates": [{
'fiscal_year': fiscal_year,
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000
}],
"accounts": [{
'company': '_Test Company',
'account': 'TDS - _TC'
}]
}).insert()

View File

@ -99,7 +99,6 @@ class ReceivablePayableReport(object):
voucher_no = gle.voucher_no, voucher_no = gle.voucher_no,
party = gle.party, party = gle.party,
posting_date = gle.posting_date, posting_date = gle.posting_date,
remarks = gle.remarks,
account_currency = gle.account_currency, account_currency = gle.account_currency,
invoiced = 0.0, invoiced = 0.0,
paid = 0.0, paid = 0.0,
@ -579,7 +578,7 @@ class ReceivablePayableReport(object):
self.gl_entries = frappe.db.sql(""" self.gl_entries = frappe.db.sql("""
select select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, remarks, {0} against_voucher_type, against_voucher, account_currency, {0}
from from
`tabGL Entry` `tabGL Entry`
where where
@ -792,8 +791,6 @@ class ReceivablePayableReport(object):
self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link',
options='Supplier Group') options='Supplier Group')
self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200)
def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120): def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120):
if not fieldname: fieldname = scrub(label) if not fieldname: fieldname = scrub(label)
if fieldtype=='Currency': options='currency' if fieldtype=='Currency': options='currency'

View File

@ -48,17 +48,18 @@ def validate_filters(filters, account_details):
if not filters.get("from_date") and not filters.get("to_date"): if not filters.get("from_date") and not filters.get("to_date"):
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
for account in filters.account:
if not account_details.get(account):
frappe.throw(_("Account {0} does not exists").format(account))
if filters.get('account'): if filters.get('account'):
filters.account = frappe.parse_json(filters.get('account')) filters.account = frappe.parse_json(filters.get('account'))
for account in filters.account:
if not account_details.get(account):
frappe.throw(_("Account {0} does not exists").format(account))
if (filters.get("account") and filters.get("group_by") == _('Group by Account') if (filters.get("account") and filters.get("group_by") == _('Group by Account')):
and account_details[filters.account].is_group == 0): filters.account = frappe.parse_json(filters.get('account'))
frappe.throw(_("Can not filter based on Account, if grouped by Account")) for account in filters.account:
if account_details[account].is_group == 0:
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
if (filters.get("voucher_no") if (filters.get("voucher_no")
and filters.get("group_by") in [_('Group by Voucher')]): and filters.get("group_by") in [_('Group by Voucher')]):

View File

@ -241,6 +241,7 @@ class GrossProfitGenerator(object):
sle.voucher_detail_no == row.item_row: sle.voucher_detail_no == row.item_row:
previous_stock_value = len(my_sle) > i+1 and \ previous_stock_value = len(my_sle) > i+1 and \
flt(my_sle[i+1].stock_value) or 0.0 flt(my_sle[i+1].stock_value) or 0.0
if previous_stock_value: if previous_stock_value:
return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
else: else:
@ -335,7 +336,7 @@ class GrossProfitGenerator(object):
res = frappe.db.sql("""select item_code, voucher_type, voucher_no, res = frappe.db.sql("""select item_code, voucher_type, voucher_no,
voucher_detail_no, stock_value, warehouse, actual_qty as qty voucher_detail_no, stock_value, warehouse, actual_qty as qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where company=%(company)s where company=%(company)s and is_cancelled = 0
order by order by
item_code desc, warehouse desc, posting_date desc, item_code desc, warehouse desc, posting_date desc,
posting_time desc, creation desc""", self.filters, as_dict=True) posting_time desc, creation desc""", self.filters, as_dict=True)

View File

@ -75,7 +75,8 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
select voucher_no, credit select voucher_no, credit
from `tabGL Entry` from `tabGL Entry`
where party in (%s) and credit > 0 where party in (%s) and credit > 0
and company=%s and posting_date between %s and %s and company=%s and is_cancelled = 0
and posting_date between %s and %s
""", (supplier, company, from_date, to_date), as_dict=1) """, (supplier, company, from_date, to_date), as_dict=1)
supplier_credit_amount = flt(sum(d.credit for d in entries)) supplier_credit_amount = flt(sum(d.credit for d in entries))

View File

@ -472,7 +472,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
"total_amount": d.grand_total, "total_amount": d.grand_total,
"outstanding_amount": d.outstanding_amount, "outstanding_amount": d.outstanding_amount,
"allocated_amount": d.allocated_amount, "allocated_amount": d.allocated_amount,
"exchange_rate": d.exchange_rate "exchange_rate": d.exchange_rate if not d.exchange_gain_loss else payment_entry.get_exchange_rate(),
"exchange_gain_loss": d.exchange_gain_loss # only populated from invoice in case of advance allocation
} }
if d.voucher_detail_no: if d.voucher_detail_no:
@ -498,12 +499,15 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
payment_entry.set_amounts() payment_entry.set_amounts()
if d.difference_amount and d.difference_account: if d.difference_amount and d.difference_account:
payment_entry.set_gain_or_loss(account_details={ account_details = {
'account': d.difference_account, 'account': d.difference_account,
'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company', 'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company',
payment_entry.company, "cost_center"), payment_entry.company, "cost_center")
'amount': d.difference_amount }
}) if d.difference_amount:
account_details['amount'] = d.difference_amount
payment_entry.set_gain_or_loss(account_details=account_details)
if not do_not_save: if not do_not_save:
payment_entry.save(ignore_permissions=True) payment_entry.save(ignore_permissions=True)
@ -962,7 +966,7 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
for e in existing_gle: for e in existing_gle:
if entry.account == e.account: if entry.account == e.account:
account_existed = True account_existed = True
if (entry.account == e.account and entry.against_account == e.against_account if (entry.account == e.account
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
and ( flt(entry.debit, precision) != flt(e.debit, precision) or and ( flt(entry.debit, precision) != flt(e.debit, precision) or
flt(entry.credit, precision) != flt(e.credit, precision))): flt(entry.credit, precision) != flt(e.credit, precision))):

View File

@ -124,6 +124,8 @@ class AccountsController(TransactionBase):
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
self.set_advances() self.set_advances()
self.set_advance_gain_or_loss()
if self.is_return: if self.is_return:
self.validate_qty() self.validate_qty()
else: else:
@ -584,15 +586,18 @@ class AccountsController(TransactionBase):
allocated_amount = min(amount - advance_allocated, d.amount) allocated_amount = min(amount - advance_allocated, d.amount)
advance_allocated += flt(allocated_amount) advance_allocated += flt(allocated_amount)
self.append("advances", { advance_row = {
"doctype": self.doctype + " Advance", "doctype": self.doctype + " Advance",
"reference_type": d.reference_type, "reference_type": d.reference_type,
"reference_name": d.reference_name, "reference_name": d.reference_name,
"reference_row": d.reference_row, "reference_row": d.reference_row,
"remarks": d.remarks, "remarks": d.remarks,
"advance_amount": flt(d.amount), "advance_amount": flt(d.amount),
"allocated_amount": allocated_amount "allocated_amount": allocated_amount,
}) "ref_exchange_rate": flt(d.exchange_rate) # exchange_rate of advance entry
}
self.append("advances", advance_row)
def get_advance_entries(self, include_unallocated=True): def get_advance_entries(self, include_unallocated=True):
if self.doctype == "Sales Invoice": if self.doctype == "Sales Invoice":
@ -650,6 +655,71 @@ class AccountsController(TransactionBase):
"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.") "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.")
.format(d.reference_name, d.against_order)) .format(d.reference_name, d.against_order))
def set_advance_gain_or_loss(self):
if not self.get("advances"):
return
for d in self.get("advances"):
advance_exchange_rate = d.ref_exchange_rate
if (d.allocated_amount and self.conversion_rate != 1
and self.conversion_rate != advance_exchange_rate):
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate
d.exchange_gain_loss = difference
def make_exchange_gain_loss_gl_entries(self, gl_entries):
if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']:
for d in self.get("advances"):
if d.exchange_gain_loss:
is_purchase_invoice = self.get('doctype') == 'Purchase Invoice'
party = self.supplier if is_purchase_invoice else self.customer
party_account = self.credit_to if is_purchase_invoice else self.debit_to
party_type = "Supplier" if is_purchase_invoice else "Customer"
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
if not gain_loss_account:
frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}")
.format(self.get('company')))
account_currency = get_account_currency(gain_loss_account)
if account_currency != self.company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency))
# for purchase
dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit'
if not is_purchase_invoice:
# just reverse for sales?
dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
gl_entries.append(
self.get_gl_dict({
"account": gain_loss_account,
"account_currency": account_currency,
"against": party,
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
dr_or_cr: abs(d.exchange_gain_loss),
"cost_center": self.cost_center,
"project": self.project
}, item=d)
)
dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
gl_entries.append(
self.get_gl_dict({
"account": party_account,
"party_type": party_type,
"party": party,
"against": gain_loss_account,
dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate),
dr_or_cr: abs(d.exchange_gain_loss),
"cost_center": self.cost_center,
"project": self.project
}, self.party_account_currency, item=self)
)
def update_against_document_in_jv(self): def update_against_document_in_jv(self):
""" """
Links invoice and advance voucher: Links invoice and advance voucher:
@ -690,7 +760,9 @@ class AccountsController(TransactionBase):
if self.party_account_currency != self.company_currency else 1), if self.party_account_currency != self.company_currency else 1),
'grand_total': (self.base_grand_total 'grand_total': (self.base_grand_total
if self.party_account_currency == self.company_currency else self.grand_total), if self.party_account_currency == self.company_currency else self.grand_total),
'outstanding_amount': self.outstanding_amount 'outstanding_amount': self.outstanding_amount,
'difference_account': frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account'),
'exchange_gain_loss': flt(d.get('exchange_gain_loss'))
}) })
lst.append(args) lst.append(args)
@ -751,11 +823,11 @@ class AccountsController(TransactionBase):
account_currency = get_account_currency(tax.account_head) account_currency = get_account_currency(tax.account_head)
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
else:
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
else:
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
party = self.supplier if self.doctype == "Purchase Invoice" else self.customer party = self.supplier if self.doctype == "Purchase Invoice" else self.customer
unallocated_amount = tax.tax_amount - tax.allocated_amount unallocated_amount = tax.tax_amount - tax.allocated_amount
@ -1045,8 +1117,11 @@ class AccountsController(TransactionBase):
for d in self.get("payment_schedule"): for d in self.get("payment_schedule"):
if d.invoice_portion: if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount'))
d.outstanding = d.payment_amount d.outstanding = d.payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount'))
def set_due_date(self): def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
@ -1289,6 +1364,8 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
party_account_field = "paid_from" if party_type == "Customer" else "paid_to" party_account_field = "paid_from" if party_type == "Customer" else "paid_to"
currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency"
payment_type = "Receive" if party_type == "Customer" else "Pay" payment_type = "Receive" if party_type == "Customer" else "Pay"
exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate"
payment_entries_against_order, unallocated_payment_entries = [], [] payment_entries_against_order, unallocated_payment_entries = [], []
limit_cond = "limit %s" % limit if limit else "" limit_cond = "limit %s" % limit if limit else ""
@ -1305,27 +1382,28 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
"Payment Entry" as reference_type, t1.name as reference_name, "Payment Entry" as reference_type, t1.name as reference_name,
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
t2.reference_name as against_order, t1.posting_date, t2.reference_name as against_order, t1.posting_date,
t1.{0} as currency t1.{0} as currency, t1.{4} as exchange_rate
from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 from `tabPayment Entry` t1, `tabPayment Entry Reference` t2
where where
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
and t2.reference_doctype = %s {2} and t2.reference_doctype = %s {2}
order by t1.posting_date {3} order by t1.posting_date {3}
""".format(currency_field, party_account_field, reference_condition, limit_cond), """.format(currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field),
[party_account, payment_type, party_type, party, [party_account, payment_type, party_type, party,
order_doctype] + order_list, as_dict=1) order_doctype] + order_list, as_dict=1)
if include_unallocated: if include_unallocated:
unallocated_payment_entries = frappe.db.sql(""" unallocated_payment_entries = frappe.db.sql("""
select "Payment Entry" as reference_type, name as reference_name, select "Payment Entry" as reference_type, name as reference_name,
remarks, unallocated_amount as amount remarks, unallocated_amount as amount, {2} as exchange_rate
from `tabPayment Entry` from `tabPayment Entry`
where where
{0} = %s and party_type = %s and party = %s and payment_type = %s {0} = %s and party_type = %s and party = %s and payment_type = %s
and docstatus = 1 and unallocated_amount > 0 and docstatus = 1 and unallocated_amount > 0
order by posting_date {1} order by posting_date {1}
""".format(party_account_field, limit_cond), (party_account, party_type, party, payment_type), as_dict=1) """.format(party_account_field, limit_cond, exchange_rate_field),
(party_account, party_type, party, payment_type), as_dict=1)
return list(payment_entries_against_order) + list(unallocated_payment_entries) return list(payment_entries_against_order) + list(unallocated_payment_entries)

View File

@ -407,6 +407,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
where where
batch.disabled = 0 batch.disabled = 0
and sle.is_cancelled = 0
and sle.item_code = %(item_code)s and sle.item_code = %(item_code)s
and sle.warehouse = %(warehouse)s and sle.warehouse = %(warehouse)s
and (sle.batch_no like %(txt)s and (sle.batch_no like %(txt)s

View File

@ -53,12 +53,17 @@ class StockController(AccountsController):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for d in self.get("items"): for d in self.get("items"):
if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no: if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no:
serial_nos = get_serial_nos(d.serial_no) serial_nos = frappe.get_all("Serial No",
for serial_no_data in frappe.get_all("Serial No", fields=["batch_no", "name", "warehouse"],
filters={"name": ("in", serial_nos)}, fields=["batch_no", "name"]): filters={
if serial_no_data.batch_no != d.batch_no: "name": ("in", get_serial_nos(d.serial_no))
}
)
for row in serial_nos:
if row.warehouse and row.batch_no != d.batch_no:
frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}") frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}")
.format(d.idx, serial_no_data.name, d.batch_no)) .format(d.idx, row.name, d.batch_no))
if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")
@ -356,42 +361,68 @@ class StockController(AccountsController):
}, update_modified) }, update_modified)
def validate_inspection(self): def validate_inspection(self):
'''Checks if quality inspection is set for Items that require inspection. """Checks if quality inspection is set/ is valid for Items that require inspection."""
On submit, throw an exception''' inspection_fieldname_map = {
inspection_required_fieldname = None "Purchase Receipt": "inspection_required_before_purchase",
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: "Purchase Invoice": "inspection_required_before_purchase",
inspection_required_fieldname = "inspection_required_before_purchase" "Sales Invoice": "inspection_required_before_delivery",
elif self.doctype in ["Delivery Note", "Sales Invoice"]: "Delivery Note": "inspection_required_before_delivery"
inspection_required_fieldname = "inspection_required_before_delivery" }
inspection_required_fieldname = inspection_fieldname_map.get(self.doctype)
# return if inspection is not required on document level
if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or
(self.doctype == "Stock Entry" and not self.inspection_required) or (self.doctype == "Stock Entry" and not self.inspection_required) or
(self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)): (self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)):
return return
for d in self.get('items'): for row in self.get('items'):
qa_required = False qi_required = False
if (inspection_required_fieldname and not d.quality_inspection and if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)):
frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)): qi_required = True
qa_required = True elif self.doctype == "Stock Entry" and row.t_warehouse:
elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse: qi_required = True # inward stock needs inspection
qa_required = True
if self.docstatus == 1 and d.quality_inspection:
qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection)
if qa_doc.docstatus == 0:
link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
if qa_doc.status != 'Accepted': if qi_required: # validate row only if inspection is required on item level
frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") self.validate_qi_presence(row)
.format(d.idx, d.item_code), QualityInspectionRejectedError) if self.docstatus == 1:
elif qa_required : self.validate_qi_submission(row)
action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted self.validate_qi_rejection(row)
if self.docstatus==1 and action == 'Stop':
frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)), def validate_qi_presence(self, row):
exc=QualityInspectionRequiredError) """Check if QI is present on row level. Warn on save and stop on submit if missing."""
else: if not row.quality_inspection:
frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code))) msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
if self.docstatus == 1:
frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
else:
frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
def validate_qi_submission(self, row):
"""Check if QI is submitted on row level, during submission"""
action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus")
if not qa_docstatus == 1:
link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
if action == "Stop":
frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
else:
frappe.msgprint(_(msg), alert=True, indicator="orange")
def validate_qi_rejection(self, row):
"""Check if QI is rejected on row level, during submission"""
action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected")
qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status")
if qa_status == "Rejected":
link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
if action == "Stop":
frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
else:
frappe.msgprint(_(msg), alert=True, indicator="orange")
def update_blanket_order(self): def update_blanket_order(self):
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))

View File

@ -152,7 +152,7 @@ class calculate_taxes_and_totals(object):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc) validate_inclusive_tax(tax, self.doc)
if not self.doc.get('is_consolidated'): if not (self.doc.get('is_consolidated') or tax.get("dont_recompute_tax")):
tax.item_wise_tax_detail = {} tax.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount", tax_fields = ["total", "tax_amount_after_discount_amount",
@ -347,7 +347,7 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Item Quantity": elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty current_tax_amount = tax_rate * item.qty
if not self.doc.get("is_consolidated"): if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")):
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount return current_tax_amount
@ -455,7 +455,8 @@ class calculate_taxes_and_totals(object):
def _cleanup(self): def _cleanup(self):
if not self.doc.get('is_consolidated'): if not self.doc.get('is_consolidated'):
for tax in self.doc.get("taxes"): for tax in self.doc.get("taxes"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) if not tax.get("dont_recompute_tax"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
def set_discount_amount(self): def set_discount_amount(self):
if self.doc.additional_discount_percentage: if self.doc.additional_discount_percentage:

View File

@ -53,6 +53,13 @@ frappe.ui.form.on("Opportunity", {
frm.get_field("items").grid.set_multiple_add("item_code", "qty"); frm.get_field("items").grid.set_multiple_add("item_code", "qty");
}, },
status:function(frm){
if (frm.doc.status == "Lost"){
frm.trigger('set_as_lost_dialog');
}
},
customer_address: function(frm, cdt, cdn) { customer_address: function(frm, cdt, cdn) {
erpnext.utils.get_address_display(frm, 'customer_address', 'address_display', false); erpnext.utils.get_address_display(frm, 'customer_address', 'address_display', false);
}, },
@ -91,11 +98,6 @@ frappe.ui.form.on("Opportunity", {
frm.add_custom_button(__('Quotation'), frm.add_custom_button(__('Quotation'),
cur_frm.cscript.create_quotation, __('Create')); cur_frm.cscript.create_quotation, __('Create'));
if(doc.status!=="Quotation") {
frm.add_custom_button(__('Lost'), () => {
frm.trigger('set_as_lost_dialog');
});
}
} }
if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) { if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) {

View File

@ -34,11 +34,14 @@ def enroll_student(source_name):
} }
}}, ignore_permissions=True) }}, ignore_permissions=True)
student.save() student.save()
student_applicant = frappe.db.get_value("Student Applicant", source_name,
["student_category", "program"], as_dict=True)
program_enrollment = frappe.new_doc("Program Enrollment") program_enrollment = frappe.new_doc("Program Enrollment")
program_enrollment.student = student.name program_enrollment.student = student.name
program_enrollment.student_category = student.student_category program_enrollment.student_category = student_applicant.student_category
program_enrollment.student_name = student.title program_enrollment.student_name = student.title
program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program") program_enrollment.program = student_applicant.program
frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user) frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user)
return program_enrollment return program_enrollment

View File

@ -1,195 +1,68 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2016-06-10 03:29:02.539914",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2016-06-10 03:29:02.539914", "field_order": [
"custom": 0, "student_applicant",
"docstatus": 0, "student",
"doctype": "DocType", "student_name",
"document_type": "", "column_break_3",
"editable_grid": 1, "student_batch_name",
"student_category"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "student_applicant",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Applicant",
"columns": 0, "options": "Student Applicant"
"depends_on": "", },
"fieldname": "student_applicant",
"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": "Student Applicant",
"length": 0,
"no_copy": 0,
"options": "Student Applicant",
"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, "fieldname": "student",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student",
"columns": 0, "options": "Student"
"depends_on": "", },
"fieldname": "student",
"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": "Student",
"length": 0,
"no_copy": 0,
"options": "Student",
"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, "fieldname": "column_break_3",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student_name",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Name",
"columns": 0, "read_only": 1
"fieldname": "student_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": "Student 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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student_batch_name",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Batch Name",
"columns": 0, "options": "Student Batch Name"
"fieldname": "student_batch_name", },
"fieldtype": "Link", {
"hidden": 0, "fieldname": "student_category",
"ignore_user_permissions": 0, "fieldtype": "Link",
"ignore_xss_filter": 0, "label": "Student Category",
"in_filter": 0, "options": "Student Category",
"in_global_search": 0, "read_only": 1
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Batch Name",
"length": 0,
"no_copy": 0,
"options": "Student Batch Name",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2021-07-29 18:19:54.471594",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Education",
"in_create": 0, "name": "Program Enrollment Tool Student",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "quick_entry": 1,
"max_attachments": 0, "restrict_to_domain": "Education",
"modified": "2018-01-02 12:03:53.890741", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC"
"module": "Education",
"name": "Program Enrollment Tool Student",
"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": 0,
"track_seen": 0
} }

View File

@ -18,5 +18,8 @@ frappe.ui.form.on('Shopify Log', {
}) })
}).addClass('btn-primary'); }).addClass('btn-primary');
} }
let app_link = "<a href='https://frappecloud.com/marketplace/apps/ecommerce-integrations' target='_blank'>Ecommerce Integrations</a>"
frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
} }
}); });

View File

@ -36,6 +36,10 @@ frappe.ui.form.on("Shopify Settings", "refresh", function(frm){
frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note); frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note);
} }
let app_link = "<a href='https://frappecloud.com/marketplace/apps/ecommerce-integrations' target='_blank'>Ecommerce Integrations</a>"
frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
}) })
$.extend(erpnext_integrations.shopify_settings, { $.extend(erpnext_integrations.shopify_settings, {

View File

@ -24,7 +24,8 @@ doctype_js = {
"Address": "public/js/address.js", "Address": "public/js/address.js",
"Communication": "public/js/communication.js", "Communication": "public/js/communication.js",
"Event": "public/js/event.js", "Event": "public/js/event.js",
"Newsletter": "public/js/newsletter.js" "Newsletter": "public/js/newsletter.js",
"Contact": "public/js/contact.js"
} }
override_doctype_class = { override_doctype_class = {
@ -429,7 +430,8 @@ regional_overrides = {
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', '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.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields', 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount' 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount',
'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code'
}, },
'United Arab Emirates': { 'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',

View File

@ -9,7 +9,7 @@ from frappe.utils import flt, getdate
from frappe import _ from frappe import _
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name from erpnext.hr.utils import set_employee_name, validate_active_employee
class Appraisal(Document): class Appraisal(Document):
def validate(self): def validate(self):
@ -19,6 +19,7 @@ class Appraisal(Document):
if not self.goals: if not self.goals:
frappe.throw(_("Goals cannot be empty")) frappe.throw(_("Goals cannot be empty"))
validate_active_employee(self.employee)
set_employee_name(self) set_employee_name(self)
self.validate_dates() self.validate_dates()
self.validate_existing_appraisal() self.validate_existing_appraisal()

View File

@ -8,11 +8,13 @@ from frappe.utils import getdate, nowdate
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr, get_datetime, formatdate from frappe.utils import cstr, get_datetime, formatdate
from erpnext.hr.utils import validate_active_employee
class Attendance(Document): class Attendance(Document):
def validate(self): def validate(self):
from erpnext.controllers.status_updater import validate_status from erpnext.controllers.status_updater import validate_status
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
validate_active_employee(self.employee)
self.validate_attendance_date() self.validate_attendance_date()
self.validate_duplicate_record() self.validate_duplicate_record()
self.validate_employee_status() self.validate_employee_status()

View File

@ -8,10 +8,11 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import date_diff, add_days, getdate from frappe.utils import date_diff, add_days, getdate
from erpnext.hr.doctype.employee.employee import is_holiday from erpnext.hr.doctype.employee.employee import is_holiday
from erpnext.hr.utils import validate_dates from erpnext.hr.utils import validate_dates, validate_active_employee
class AttendanceRequest(Document): class AttendanceRequest(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_dates(self, self.from_date, self.to_date) validate_dates(self, self.from_date, self.to_date)
if self.half_day: if self.half_day:
if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date): if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date):

View File

@ -7,12 +7,13 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import date_diff, add_days, getdate, cint, format_date from frappe.utils import date_diff, add_days, getdate, cint, format_date
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \
get_holidays_for_employee, create_additional_leave_ledger_entry get_holidays_for_employee, create_additional_leave_ledger_entry
class CompensatoryLeaveRequest(Document): class CompensatoryLeaveRequest(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_dates(self, self.work_from_date, self.work_end_date) validate_dates(self, self.work_from_date, self.work_end_date)
if self.half_day: if self.half_day:
if not self.half_day_date: if not self.half_day_date:

View File

@ -13,8 +13,10 @@ from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeUserDisabledError(frappe.ValidationError):
class EmployeeLeftValidationError(frappe.ValidationError): pass pass
class InactiveEmployeeStatusError(frappe.ValidationError):
pass
class Employee(NestedSet): class Employee(NestedSet):
nsm_parent_field = 'reports_to' nsm_parent_field = 'reports_to'
@ -196,7 +198,7 @@ class Employee(NestedSet):
message += "<br><br><ul><li>" + "</li><li>".join(link_to_employees) message += "<br><br><ul><li>" + "</li><li>".join(link_to_employees)
message += "</li></ul><br>" message += "</li></ul><br>"
message += _("Please make sure the employees above report to another Active employee.") message += _("Please make sure the employees above report to another Active employee.")
throw(message, EmployeeLeftValidationError, _("Cannot Relieve Employee")) throw(message, InactiveEmployeeStatusError, _("Cannot Relieve Employee"))
if not self.relieving_date: if not self.relieving_date:
throw(_("Please enter relieving date.")) throw(_("Please enter relieving date."))

View File

@ -7,7 +7,7 @@ import frappe
import erpnext import erpnext
import unittest import unittest
import frappe.utils import frappe.utils
from erpnext.hr.doctype.employee.employee import EmployeeLeftValidationError from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError
test_records = frappe.get_test_records('Employee') test_records = frappe.get_test_records('Employee')
@ -45,10 +45,33 @@ class TestEmployee(unittest.TestCase):
employee2_doc.save() employee2_doc.save()
employee1_doc.reload() employee1_doc.reload()
employee1_doc.status = 'Left' employee1_doc.status = 'Left'
self.assertRaises(EmployeeLeftValidationError, employee1_doc.save) self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save)
def test_employee_status_inactive(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
employee = make_employee("test_employee_status@company.com")
employee_doc = frappe.get_doc("Employee", employee)
employee_doc.status = "Inactive"
employee_doc.save()
employee_doc.reload()
make_holiday_list()
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List")
frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""")
salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly",
employee=employee_doc.name, company=employee_doc.company)
salary_slip = make_salary_slip(salary_structure.name, employee=employee_doc.name)
self.assertRaises(InactiveEmployeeStatusError, salary_slip.save)
def tearDown(self):
frappe.db.rollback()
def make_employee(user, company=None, **kwargs): def make_employee(user, company=None, **kwargs):
""
if not frappe.db.get_value("User", user): if not frappe.db.get_value("User", user):
frappe.get_doc({ frappe.get_doc({
"doctype": "User", "doctype": "User",
@ -80,4 +103,5 @@ def make_employee(user, company=None, **kwargs):
employee.insert() employee.insert()
return employee.name return employee.name
else: else:
frappe.db.set_value("Employee", {"employee_name":user}, "status", "Active")
return frappe.get_value("Employee", {"employee_name":user}, "name") return frappe.get_value("Employee", {"employee_name":user}, "name")

View File

@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.hr.utils import validate_active_employee
class EmployeeAdvanceOverPayment(frappe.ValidationError): class EmployeeAdvanceOverPayment(frappe.ValidationError):
pass pass
@ -18,11 +19,11 @@ class EmployeeAdvance(Document):
'make_payment_via_journal_entry') 'make_payment_via_journal_entry')
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.set_status() self.set_status()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry') self.ignore_linked_doctypes = ('GL Entry')
self.set_status()
def set_status(self): def set_status(self):
if self.docstatus == 0: if self.docstatus == 0:
@ -183,9 +184,9 @@ def make_return_entry(employee, company, employee_advance_name, return_amount,
bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
if not bank_cash_account: if not bank_cash_account:
frappe.throw(_("Please set a Default Cash Account in Company defaults")) frappe.throw(_("Please set a Default Cash Account in Company defaults"))
advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency') advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency')
je = frappe.new_doc('Journal Entry') je = frappe.new_doc('Journal Entry')
je.posting_date = nowdate() je.posting_date = nowdate()
je.voucher_type = get_voucher_type(mode_of_payment) je.voucher_type = get_voucher_type(mode_of_payment)
@ -229,4 +230,4 @@ def get_voucher_type(mode_of_payment=None):
if mode_of_payment_type == "Bank": if mode_of_payment_type == "Bank":
voucher_type = "Bank Entry" voucher_type = "Bank Entry"
return voucher_type return voucher_type

View File

@ -9,9 +9,11 @@ from frappe.model.document import Document
from frappe import _ from frappe import _
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift
from erpnext.hr.utils import validate_active_employee
class EmployeeCheckin(Document): class EmployeeCheckin(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_duplicate_log() self.validate_duplicate_log()
self.fetch_shift() self.fetch_shift()
@ -122,7 +124,7 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
"""Given a set of logs in chronological order calculates the total working hours based on the parameters. """Given a set of logs in chronological order calculates the total working hours based on the parameters.
Zero is returned for all invalid cases. Zero is returned for all invalid cases.
:param logs: The List of 'Employee Checkin'. :param logs: The List of 'Employee Checkin'.
:param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin' :param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin'
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out' :param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'

View File

@ -7,12 +7,11 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate from frappe.utils import getdate
from erpnext.hr.utils import update_employee from erpnext.hr.utils import update_employee, validate_active_employee
class EmployeePromotion(Document): class EmployeePromotion(Document):
def validate(self): def validate(self):
if frappe.get_value("Employee", self.employee, "status") != "Active": validate_active_employee(self.employee)
frappe.throw(_("Cannot promote Employee with status Left or Inactive"))
def before_submit(self): def before_submit(self):
if getdate(self.promotion_date) > getdate(): if getdate(self.promotion_date) > getdate():

View File

@ -7,9 +7,11 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import get_link_to_form from frappe.utils import get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_active_employee
class EmployeeReferral(Document): class EmployeeReferral(Document):
def validate(self): def validate(self):
validate_active_employee(self.referrer)
self.set_full_name() self.set_full_name()
self.set_referral_bonus_payment_status() self.set_referral_bonus_payment_status()

View File

@ -10,10 +10,6 @@ from frappe.utils import getdate
from erpnext.hr.utils import update_employee from erpnext.hr.utils import update_employee
class EmployeeTransfer(Document): class EmployeeTransfer(Document):
def validate(self):
if frappe.get_value("Employee", self.employee, "status") != "Active":
frappe.throw(_("Cannot transfer Employee with status Left or Inactive"))
def before_submit(self): def before_submit(self):
if getdate(self.transfer_date) > getdate(): if getdate(self.transfer_date) > getdate():
frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"), frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"),

View File

@ -6,7 +6,7 @@ import frappe, erpnext
from frappe import _ from frappe import _
from frappe.utils import get_fullname, flt, cstr, get_link_to_form from frappe.utils import get_fullname, flt, cstr, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name, share_doc_with_approver from erpnext.hr.utils import set_employee_name, share_doc_with_approver, validate_active_employee
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
@ -23,6 +23,7 @@ class ExpenseClaim(AccountsController):
'make_payment_via_journal_entry') 'make_payment_via_journal_entry')
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_advances() self.validate_advances()
self.validate_sanctioned_amount() self.validate_sanctioned_amount()
self.calculate_total_amount() self.calculate_total_amount()
@ -35,8 +36,8 @@ class ExpenseClaim(AccountsController):
if self.task and not self.project: if self.task and not self.project:
self.project = frappe.db.get_value("Task", self.task, "project") self.project = frappe.db.get_value("Task", self.task, "project")
def set_status(self): def set_status(self, update=False):
self.status = { status = {
"0": "Draft", "0": "Draft",
"1": "Submitted", "1": "Submitted",
"2": "Cancelled" "2": "Cancelled"
@ -44,14 +45,18 @@ class ExpenseClaim(AccountsController):
paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
precision = self.precision("grand_total") precision = self.precision("grand_total")
if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1
and flt(self.grand_total, precision) == flt(paid_amount, precision))) \ and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved':
and self.docstatus == 1 and self.approval_status == 'Approved': status = "Paid"
self.status = "Paid"
elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved':
self.status = "Unpaid" status = "Unpaid"
elif self.docstatus == 1 and self.approval_status == 'Rejected': elif self.docstatus == 1 and self.approval_status == 'Rejected':
self.status = 'Rejected' status = 'Rejected'
if update:
self.db_set("status", status)
else:
self.status = status
def on_update(self): def on_update(self):
share_doc_with_approver(self, self.expense_approver) share_doc_with_approver(self, self.expense_approver)
@ -74,7 +79,7 @@ class ExpenseClaim(AccountsController):
if self.is_paid: if self.is_paid:
update_reimbursed_amount(self) update_reimbursed_amount(self)
self.set_status() self.set_status(update=True)
self.update_claimed_amount_in_employee_advance() self.update_claimed_amount_in_employee_advance()
def on_cancel(self): def on_cancel(self):
@ -86,7 +91,6 @@ class ExpenseClaim(AccountsController):
if self.is_paid: if self.is_paid:
update_reimbursed_amount(self) update_reimbursed_amount(self)
self.set_status()
self.update_claimed_amount_in_employee_advance() self.update_claimed_amount_in_employee_advance()
def update_claimed_amount_in_employee_advance(self): def update_claimed_amount_in_employee_advance(self):

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate
from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver, validate_active_employee
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange
@ -22,6 +22,7 @@ class LeaveApplication(Document):
return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type) return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type)
def validate(self): def validate(self):
validate_active_employee(self.employee)
set_employee_name(self) set_employee_name(self)
self.validate_dates() self.validate_dates()
self.validate_balance_leaves() self.validate_balance_leaves()

View File

@ -7,7 +7,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate, nowdate, flt from frappe.utils import getdate, nowdate, flt
from erpnext.hr.utils import set_employee_name from erpnext.hr.utils import set_employee_name, validate_active_employee
from erpnext.payroll.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_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
@ -15,6 +15,7 @@ from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leav
class LeaveEncashment(Document): class LeaveEncashment(Document):
def validate(self): def validate(self):
set_employee_name(self) set_employee_name(self)
validate_active_employee(self.employee)
self.get_leave_details_for_encashment() self.get_leave_details_for_encashment()
self.validate_salary_structure() self.validate_salary_structure()

View File

@ -9,10 +9,12 @@ from frappe.model.document import Document
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from erpnext.hr.utils import validate_active_employee
from datetime import timedelta, datetime from datetime import timedelta, datetime
class ShiftAssignment(Document): class ShiftAssignment(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_overlapping_dates() self.validate_overlapping_dates()
if self.end_date and self.end_date <= self.start_date: if self.end_date and self.end_date <= self.start_date:

View File

@ -7,12 +7,13 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import formatdate, getdate from frappe.utils import formatdate, getdate
from erpnext.hr.utils import share_doc_with_approver from erpnext.hr.utils import share_doc_with_approver, validate_active_employee
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class ShiftRequest(Document): class ShiftRequest(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_dates() self.validate_dates()
self.validate_shift_request_overlap_dates() self.validate_shift_request_overlap_dates()
self.validate_approver() self.validate_approver()

View File

@ -5,6 +5,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_active_employee
class TravelRequest(Document): class TravelRequest(Document):
pass def validate(self):
validate_active_employee(self.employee)

View File

@ -3,13 +3,12 @@
import erpnext import erpnext
import frappe import frappe
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee, InactiveEmployeeStatusError
from frappe import _ from frappe import _
from frappe.desk.form import assign_to from frappe.desk.form import assign_to
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate, from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate,
get_datetime, getdate, nowdate, today, unique) get_datetime, getdate, nowdate, today, unique, get_link_to_form)
class DuplicateDeclarationError(frappe.ValidationError): pass class DuplicateDeclarationError(frappe.ValidationError): pass
@ -20,6 +19,7 @@ class EmployeeBoardingController(Document):
Assign to the concerned person and roles as per the onboarding/separation template Assign to the concerned person and roles as per the onboarding/separation template
''' '''
def validate(self): def validate(self):
validate_active_employee(self.employee)
# remove the task if linked before submitting the form # remove the task if linked before submitting the form
if self.amended_from: if self.amended_from:
for activity in self.activities: for activity in self.activities:
@ -522,3 +522,8 @@ def share_doc_with_approver(doc, user):
approver = approvers.get(doc.doctype) approver = approvers.get(doc.doctype)
if doc_before_save.get(approver) != doc.get(approver): if doc_before_save.get(approver) != doc.get(approver):
frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver)) frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver))
def validate_active_employee(employee):
if frappe.db.get_value("Employee", employee, "status") == "Inactive":
frappe.throw(_("Transactions cannot be created for an Inactive Employee {0}.").format(
get_link_to_form("Employee", employee)), InactiveEmployeeStatusError)

View File

@ -28,7 +28,8 @@ frappe.ui.form.on('Loan', {
frm.set_query("loan_type", function () { frm.set_query("loan_type", function () {
return { return {
"filters": { "filters": {
"docstatus": 1 "docstatus": 1,
"company": frm.doc.company
} }
}; };
}); });

View File

@ -14,6 +14,13 @@ frappe.ui.form.on('Loan Application', {
refresh: function(frm) { refresh: function(frm) {
frm.trigger("toggle_fields"); frm.trigger("toggle_fields");
frm.trigger("add_toolbar_buttons"); frm.trigger("add_toolbar_buttons");
frm.set_query('loan_type', () => {
return {
filters: {
company: frm.doc.company
}
};
});
}, },
repayment_method: function(frm) { repayment_method: function(frm) {
frm.doc.repayment_amount = frm.doc.repayment_periods = "" frm.doc.repayment_amount = frm.doc.repayment_periods = ""

View File

@ -83,7 +83,7 @@ frappe.ui.form.on("BOM", {
if (!frm.doc.__islocal && frm.doc.docstatus<2) { if (!frm.doc.__islocal && frm.doc.docstatus<2) {
frm.add_custom_button(__("Update Cost"), function() { frm.add_custom_button(__("Update Cost"), function() {
frm.events.update_cost(frm); frm.events.update_cost(frm, true);
}); });
frm.add_custom_button(__("Browse BOM"), function() { frm.add_custom_button(__("Browse BOM"), function() {
frappe.route_options = { frappe.route_options = {
@ -318,14 +318,15 @@ frappe.ui.form.on("BOM", {
}) })
}, },
update_cost: function(frm) { update_cost: function(frm, save_doc=false) {
return frappe.call({ return frappe.call({
doc: frm.doc, doc: frm.doc,
method: "update_cost", method: "update_cost",
freeze: true, freeze: true,
args: { args: {
update_parent: true, update_parent: true,
from_child_bom:false save: save_doc,
from_child_bom: false
}, },
callback: function(r) { callback: function(r) {
refresh_field("items"); refresh_field("items");

View File

@ -330,7 +330,7 @@ class BOM(WebsiteGenerator):
frappe.get_doc("BOM", bom).update_cost(from_child_bom=True) frappe.get_doc("BOM", bom).update_cost(from_child_bom=True)
if not from_child_bom: if not from_child_bom:
frappe.msgprint(_("Cost Updated")) frappe.msgprint(_("Cost Updated"), alert=True)
def update_parent_cost(self): def update_parent_cost(self):
if self.total_cost: if self.total_cost:
@ -713,7 +713,8 @@ def get_bom_item_rate(args, bom_doc):
"conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function "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, "conversion_factor": args.get("conversion_factor") or 1,
"plc_conversion_rate": 1, "plc_conversion_rate": 1,
"ignore_party": True "ignore_party": True,
"ignore_conversion_rate": True
}) })
item_doc = frappe.get_cached_doc("Item", args.get("item_code")) item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
out = frappe._dict() out = frappe._dict()
@ -747,7 +748,7 @@ def get_valuation_rate(args):
if valuation_rate <= 0: if valuation_rate <= 0:
last_valuation_rate = frappe.db.sql("""select valuation_rate last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where item_code = %s and valuation_rate > 0 where item_code = %s and valuation_rate > 0 and is_cancelled = 0
order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code']) order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code'])
valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
@ -773,7 +774,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
item.image, item.image,
bom.project, bom.project,
bom_item.rate, bom_item.rate,
bom_item.amount, sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount,
item.stock_uom, item.stock_uom,
item.item_group, item.item_group,
item.allow_alternative_item, item.allow_alternative_item,
@ -1068,13 +1069,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if barcodes: if barcodes:
or_cond_filters["name"] = ("in", 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"): if filters and filters.get("item_code"):
has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants") has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants")
if not has_variants: if not has_variants:
@ -1083,7 +1077,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if filters and filters.get("is_stock_item"): if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1 query_filters["is_stock_item"] = 1
return frappe.get_all("Item", return frappe.get_list("Item",
fields = fields, filters=query_filters, fields = fields, filters=query_filters,
or_filters = or_cond_filters, order_by=order_by, or_filters = or_cond_filters, order_by=order_by,
limit_start=start, limit_page_length=page_len, as_list=1) limit_start=start, limit_page_length=page_len, as_list=1)

View File

@ -192,11 +192,11 @@ class JobCard(Document):
"completed_qty": args.get("completed_qty") or 0.0 "completed_qty": args.get("completed_qty") or 0.0
}) })
elif args.get("start_time"): elif args.get("start_time"):
new_args = { new_args = frappe._dict({
"from_time": get_datetime(args.get("start_time")), "from_time": get_datetime(args.get("start_time")),
"operation": args.get("sub_operation"), "operation": args.get("sub_operation"),
"completed_qty": 0.0 "completed_qty": 0.0
} })
if employees: if employees:
for name in employees: for name in employees:

View File

@ -747,9 +747,8 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
group by item_code, warehouse group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
def get_warehouse_list(warehouses, warehouse_list=None): def get_warehouse_list(warehouses):
if not warehouse_list: warehouse_list = []
warehouse_list = []
if isinstance(warehouses, str): if isinstance(warehouses, str):
warehouses = json.loads(warehouses) warehouses = json.loads(warehouses)
@ -761,23 +760,19 @@ def get_warehouse_list(warehouses, warehouse_list=None):
else: else:
warehouse_list.append(row.get("warehouse")) warehouse_list.append(row.get("warehouse"))
return warehouse_list
@frappe.whitelist() @frappe.whitelist()
def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
if isinstance(doc, str): if isinstance(doc, str):
doc = frappe._dict(json.loads(doc)) doc = frappe._dict(json.loads(doc))
warehouse_list = []
if warehouses: if warehouses:
get_warehouse_list(warehouses, warehouse_list) warehouses = list(set(get_warehouse_list(warehouses)))
if warehouse_list:
warehouses = list(set(warehouse_list))
if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses: if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses:
warehouses.remove(doc.get("for_warehouse")) warehouses.remove(doc.get("for_warehouse"))
warehouse_list = None
doc['mr_items'] = [] doc['mr_items'] = []
po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items') po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items')

View File

@ -10,7 +10,7 @@ from erpnext.stock.doctype.item.test_item import create_item
from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list
class TestProductionPlan(unittest.TestCase): class TestProductionPlan(unittest.TestCase):
def setUp(self): def setUp(self):
@ -251,6 +251,27 @@ class TestProductionPlan(unittest.TestCase):
pln.cancel() pln.cancel()
frappe.delete_doc("Production Plan", pln.name) frappe.delete_doc("Production Plan", pln.name)
def test_get_warehouse_list_group(self):
"""Check if required warehouses are returned"""
warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]'
warehouses = set(get_warehouse_list(warehouse_json))
expected_warehouses = {"_Test Warehouse Group-C1 - _TC", "_Test Warehouse Group-C2 - _TC"}
missing_warehouse = expected_warehouses - warehouses
self.assertTrue(len(missing_warehouse) == 0,
msg=f"Following warehouses were expected {', '.join(missing_warehouse)}")
def test_get_warehouse_list_single(self):
warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]'
warehouses = set(get_warehouse_list(warehouse_json))
expected_warehouses = {"_Test Scrap Warehouse - _TC", }
self.assertEqual(warehouses, expected_warehouses)
def create_production_plan(**args): def create_production_plan(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -19,6 +19,7 @@
"options": "Operation" "options": "Operation"
}, },
{ {
"default": "0",
"description": "Time in mins", "description": "Time in mins",
"fieldname": "time_in_mins", "fieldname": "time_in_mins",
"fieldtype": "Float", "fieldtype": "Float",
@ -38,7 +39,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-07 18:09:18.005578", "modified": "2021-07-15 16:39:41.635362",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Sub Operation", "name": "Sub Operation",

View File

@ -513,6 +513,60 @@ class TestWorkOrder(unittest.TestCase):
work_order1.save() work_order1.save()
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
def test_batch_size_for_fg_item(self):
fg_item = "Test Batch Size Item For BOM 3"
rm1 = "Test Batch Size Item RM 1 For BOM 3"
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0)
for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]:
item_args = {
"include_item_in_manufacturing": 1,
"is_stock_item": 1
}
if item == fg_item:
item_args['has_batch_no'] = 1
item_args['create_new_batch'] = 1
item_args['batch_number_series'] = 'TBSI3.#####'
make_item(item, item_args)
bom_name = frappe.db.get_value("BOM",
{"item": fg_item, "is_active": 1, "with_operations": 1}, "name")
if not bom_name:
bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True)
bom.save()
bom.submit()
bom_name = bom.name
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1)
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
for row in ste1.get('items'):
if row.is_finished_item:
self.assertEqual(row.item_code, fg_item)
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1)
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1)
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
for row in ste1.get('items'):
if row.is_finished_item:
self.assertEqual(row.item_code, fg_item)
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(),
qty=30, do_not_save = True)
work_order.batch_size = 10
work_order.insert()
work_order.submit()
self.assertEqual(work_order.has_batch_no, 1)
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30))
for row in ste1.get('items'):
if row.is_finished_item:
self.assertEqual(row.item_code, fg_item)
self.assertEqual(row.qty, 10)
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0)
def test_partial_material_consumption(self): def test_partial_material_consumption(self):
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1)
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)

View File

@ -239,7 +239,7 @@ class WorkOrder(Document):
self.create_serial_no_batch_no() self.create_serial_no_batch_no()
def on_submit(self): def on_submit(self):
if not self.wip_warehouse: if not self.wip_warehouse and not self.skip_transfer:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
if not self.fg_warehouse: if not self.fg_warehouse:
frappe.throw(_("For Warehouse is required before Submit")) frappe.throw(_("For Warehouse is required before Submit"))
@ -487,21 +487,20 @@ class WorkOrder(Document):
return return
operations = [] operations = []
if not self.use_multi_level_bom:
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") if self.use_multi_level_bom:
operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
else:
bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation() bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation()
bom_traversal = list(reversed(bom_tree.level_order_traversal())) bom_traversal = reversed(bom_tree.level_order_traversal())
bom_traversal.append(bom_tree) # add operation on top level item last
for d in bom_traversal: for node in bom_traversal:
if d.is_bom: if node.is_bom:
operations.extend(_get_operations(d.name, qty=d.exploded_qty)) operations.extend(_get_operations(node.name, qty=node.exploded_qty))
for correct_index, operation in enumerate(operations, start=1): bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
operation.idx = correct_index operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
for correct_index, operation in enumerate(operations, start=1):
operation.idx = correct_index
self.set('operations', operations) self.set('operations', operations)
self.calculate_time() self.calculate_time()
@ -656,7 +655,7 @@ class WorkOrder(Document):
for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999): for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999):
self.append('required_items', { self.append('required_items', {
'rate': item.rate, 'rate': item.rate,
'amount': item.amount, 'amount': item.rate * item.qty,
'operation': item.operation or operation, 'operation': item.operation or operation,
'item_code': item.item_code, 'item_code': item.item_code,
'item_name': item.item_name, 'item_name': item.item_name,

View File

@ -26,7 +26,7 @@
"razorpay_details_section", "razorpay_details_section",
"subscription_id", "subscription_id",
"customer_id", "customer_id",
"subscription_activated", "subscription_status",
"column_break_21", "column_break_21",
"subscription_start", "subscription_start",
"subscription_end" "subscription_end"
@ -151,12 +151,6 @@
"fieldname": "column_break_21", "fieldname": "column_break_21",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"default": "0",
"fieldname": "subscription_activated",
"fieldtype": "Check",
"label": "Subscription Activated"
},
{ {
"fieldname": "subscription_start", "fieldname": "subscription_start",
"fieldtype": "Date", "fieldtype": "Date",
@ -166,11 +160,17 @@
"fieldname": "subscription_end", "fieldname": "subscription_end",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Subscription End" "label": "Subscription End"
},
{
"fieldname": "subscription_status",
"fieldtype": "Select",
"label": "Subscription Status",
"options": "\nActive\nHalted"
} }
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-11-09 12:12:10.174647", "modified": "2021-07-11 14:27:26.368039",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Member", "name": "Member",

View File

@ -84,7 +84,9 @@ def create_member(user_details):
"email_id": user_details.email, "email_id": user_details.email,
"pan_number": user_details.pan or None, "pan_number": user_details.pan or None,
"membership_type": user_details.plan_id, "membership_type": user_details.plan_id,
"subscription_id": user_details.subscription_id or None "customer_id": user_details.customer_id or None,
"subscription_id": user_details.subscription_id or None,
"subscription_status": user_details.subscription_status or ""
}) })
member.insert(ignore_permissions=True) member.insert(ignore_permissions=True)

View File

@ -196,11 +196,14 @@ def make_invoice(membership, member, plan, settings):
return invoice return invoice
def get_member_based_on_subscription(subscription_id, email): def get_member_based_on_subscription(subscription_id, email=None, customer_id=None):
members = frappe.get_all("Member", filters={ filters = {"subscription_id": subscription_id}
"subscription_id": subscription_id, if email:
"email_id": email filters.update({"email_id": email})
}, order_by="creation desc") if customer_id:
filters.update({"customer_id": customer_id})
members = frappe.get_all("Member", filters=filters, order_by="creation desc")
try: try:
return frappe.get_doc("Member", members[0]["name"]) return frappe.get_doc("Member", members[0]["name"])
@ -209,8 +212,6 @@ def get_member_based_on_subscription(subscription_id, email):
def verify_signature(data, endpoint="Membership"): def verify_signature(data, endpoint="Membership"):
if frappe.flags.in_test or os.environ.get("CI"):
return True
signature = frappe.request.headers.get("X-Razorpay-Signature") signature = frappe.request.headers.get("X-Razorpay-Signature")
settings = frappe.get_doc("Non Profit Settings") settings = frappe.get_doc("Non Profit Settings")
@ -225,16 +226,7 @@ def verify_signature(data, endpoint="Membership"):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs): def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True) data = frappe.request.get_data(as_text=True)
try: data = process_request_data(data)
verify_signature(data)
except Exception as e:
log = frappe.log_error(e, "Membership Webhook Verification Error")
notify_failure(log)
return { "status": "Failed", "reason": e}
if isinstance(data, six.string_types):
data = json.loads(data)
data = frappe._dict(data)
subscription = data.payload.get("subscription", {}).get("entity", {}) subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription) subscription = frappe._dict(subscription)
@ -281,7 +273,7 @@ def trigger_razorpay_subscription(*args, **kwargs):
# Update membership values # Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at)
member.subscription_activated = 1 member.subscription_status = "Active"
member.flags.ignore_mandatory = True member.flags.ignore_mandatory = True
member.save() member.save()
@ -294,9 +286,67 @@ def trigger_razorpay_subscription(*args, **kwargs):
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id) message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log) notify_failure(log)
return { "status": "Failed", "reason": e} return {"status": "Failed", "reason": e}
return { "status": "Success" } return {"status": "Success"}
@frappe.whitelist(allow_guest=True)
def update_halted_razorpay_subscription(*args, **kwargs):
"""
When all retries have been exhausted, Razorpay moves the subscription to the halted state.
The customer has to manually retry the charge or change the card linked to the subscription,
for the subscription to move back to the active state.
"""
if frappe.request:
data = frappe.request.get_data(as_text=True)
data = process_request_data(data)
elif frappe.flags.in_test:
data = kwargs.get("data")
data = frappe._dict(data)
else:
return
if not data.event == "subscription.halted":
return
subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription)
try:
member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id)
if not member:
frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id))
member.subscription_status = "Halted"
member.flags.ignore_mandatory = True
member.save()
if subscription.get("notes"):
member = get_additional_notes(member, subscription)
except Exception as e:
message = "{0}\n\n{1}".format(e, frappe.get_traceback())
log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name))
notify_failure(log)
return {"status": "Failed", "reason": e}
return {"status": "Success"}
def process_request_data(data):
try:
verify_signature(data)
except Exception as e:
log = frappe.log_error(e, "Membership Webhook Verification Error")
notify_failure(log)
return {"status": "Failed", "reason": e}
if isinstance(data, six.string_types):
data = json.loads(data)
data = frappe._dict(data)
return data
def get_company_for_memberships(): def get_company_for_memberships():
@ -362,4 +412,4 @@ def set_expired_status():
`tabMembership` SET `status` = 'Expired' `tabMembership` SET `status` = 'Expired'
WHERE WHERE
`status` not in ('Cancelled') AND `to_date` < %s `status` not in ('Cancelled') AND `to_date` < %s
""", (nowdate())) """, (nowdate()))

View File

@ -6,6 +6,7 @@ import unittest
import frappe import frappe
import erpnext import erpnext
from erpnext.non_profit.doctype.member.member import create_member from erpnext.non_profit.doctype.member.member import create_member
from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription
from frappe.utils import nowdate, add_months from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase): class TestMembership(unittest.TestCase):
@ -13,11 +14,16 @@ class TestMembership(unittest.TestCase):
plan = setup_membership() plan = setup_membership()
# make test member # make test member
self.member_doc = create_member(frappe._dict({ self.member_doc = create_member(
'fullname': "_Test_Member", frappe._dict({
'email': "_test_member_erpnext@example.com", "fullname": "_Test_Member",
'plan_id': plan.name "email": "_test_member_erpnext@example.com",
})) "plan_id": plan.name,
"subscription_id": "sub_DEX6xcJ1HSW4CR",
"customer_id": "cust_C0WlbKhp3aLA7W",
"subscription_status": "Active"
})
)
self.member_doc.make_customer_and_link() self.member_doc.make_customer_and_link()
self.member = self.member_doc.name self.member = self.member_doc.name
@ -51,6 +57,20 @@ class TestMembership(unittest.TestCase):
"to_date": add_months(nowdate(), 3), "to_date": add_months(nowdate(), 3),
}) })
def test_halted_memberships(self):
make_membership(self.member, {
"from_date": add_months(nowdate(), 2),
"to_date": add_months(nowdate(), 3)
})
self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active")
payload = get_subscription_payload()
update_halted_razorpay_subscription(data=payload)
self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted")
def tearDown(self):
frappe.db.rollback()
def set_config(key, value): def set_config(key, value):
frappe.db.set_value("Non Profit Settings", None, key, value) frappe.db.set_value("Non Profit Settings", None, key, value)
@ -115,4 +135,28 @@ def setup_membership():
else: else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
return plan return plan
def get_subscription_payload():
return {
"entity": "event",
"account_id": "acc_BFQ7uQEaa7j2z7",
"event": "subscription.halted",
"contains": [
"subscription"
],
"payload": {
"subscription": {
"entity": {
"id": "sub_DEX6xcJ1HSW4CR",
"entity": "subscription",
"plan_id": "_rzpy_test_milythm",
"customer_id": "cust_C0WlbKhp3aLA7W",
"status": "halted",
"notes": {
"Important": "Notes for Internal Reference"
},
}
}
}
}

View File

@ -291,3 +291,10 @@ erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_amt_in_work_order_required_items
erpnext.patches.v13_0.update_export_type_for_gst
erpnext.patches.v13_0.update_tds_check_field #3
erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning

View File

@ -0,0 +1,112 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.utils import cstr, flt, cint
from erpnext.stock.stock_ledger import make_sl_entries
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
def execute():
if not frappe.db.has_column('Work Order', 'has_batch_no'):
return
frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings')
if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')):
return
frappe.reload_doc('manufacturing', 'doctype', 'work_order')
filters = {
'docstatus': 1,
'produced_qty': ('>', 0),
'creation': ('>=', '2021-06-29 00:00:00'),
'has_batch_no': 1
}
fields = ['name', 'production_item']
work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)]
if not work_orders:
return
repost_stock_entries = []
stock_entries = frappe.db.sql_list('''
SELECT
se.name
FROM
`tabStock Entry` se
WHERE
se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in %s
and not exists(
select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1
)
ORDER BY
se.posting_date, se.posting_time
''', (work_orders,))
if stock_entries:
print('Length of stock entries', len(stock_entries))
for stock_entry in stock_entries:
doc = frappe.get_doc('Stock Entry', stock_entry)
doc.set_work_order_details()
doc.load_items_from_bom()
doc.calculate_rate_and_amount()
set_expense_account(doc)
doc.make_batches('t_warehouse')
if doc.docstatus == 0:
doc.save()
else:
repost_stock_entry(doc)
repost_stock_entries.append(doc)
for repost_doc in repost_stock_entries:
repost_future_sle_and_gle(repost_doc)
def set_expense_account(doc):
for row in doc.items:
if row.is_finished_item and not row.expense_account:
row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account')
def repost_stock_entry(doc):
doc.db_update()
for child_row in doc.items:
if child_row.is_finished_item:
child_row.db_update()
sl_entries = []
finished_item_row = doc.get_finished_item_row()
get_sle_for_target_warehouse(doc, sl_entries, finished_item_row)
if sl_entries:
try:
make_sl_entries(sl_entries, True)
except Exception:
print(f'SLE entries not posted for the stock entry {doc.name}')
traceback = frappe.get_traceback()
frappe.log_error(traceback)
def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row):
for d in doc.get('items'):
if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name:
sle = doc.get_sl_entries(d, {
"warehouse": cstr(d.t_warehouse),
"actual_qty": flt(d.transfer_qty),
"incoming_rate": flt(d.valuation_rate)
})
sle.recalculate_rate = 1
sl_entries.append(sle)
def repost_future_sle_and_gle(doc):
args = frappe._dict({
"posting_date": doc.posting_date,
"posting_time": doc.posting_time,
"voucher_type": doc.doctype,
"voucher_no": doc.name,
"company": doc.company
})
create_repost_item_valuation_entry(args)

View File

@ -37,7 +37,7 @@ def execute():
if frappe.db.exists('DocType', 'Opportunity'): if frappe.db.exists('DocType', 'Opportunity'):
opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc') opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc')
frappe.reload_doc('crm', 'doctype', 'opportunity') frappe.reload_doctype('Opportunity', force=True)
rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') rename_field('Opportunity', 'mins_to_first_response', 'first_response_time')
# change fieldtype to duration # change fieldtype to duration

View File

@ -0,0 +1,15 @@
import click
import frappe
def execute():
frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings")
if not frappe.db.get_single_value("Shopify Settings", "enable_shopify"):
return
click.secho(
"Shopify Integration is moved to a separate app and will be removed from ERPNext in version-14.\n"
"Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations",
fg="yellow",
)

View File

@ -0,0 +1,10 @@
import frappe
def execute():
""" Correct amount in child table of required items table."""
frappe.reload_doc("manufacturing", "doctype", "work_order")
frappe.reload_doc("manufacturing", "doctype", "work_order_item")
frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""")

View File

@ -0,0 +1,24 @@
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
# Update custom fields
fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'})
if fieldname:
frappe.db.set_value('Custom Field', fieldname, 'default', '')
fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'})
if fieldname:
frappe.db.set_value('Custom Field', fieldname, 'default', '')
# Update Customer/Supplier Masters
frappe.db.sql("""
UPDATE `tabCustomer` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas', 'Deemed Export')
""")
frappe.db.sql("""
UPDATE `tabSupplier` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas')
""")

View File

@ -0,0 +1,21 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("setup", "doctype", "Email Digest")
frappe.reload_doc("setup", "doctype", "Email Digest Recipient")
email_digests = frappe.db.get_list('Email Digest', fields=['name', 'recipient_list'])
for email_digest in email_digests:
if email_digest.recipient_list:
for recipient in email_digest.recipient_list.split("\n"):
doc = frappe.get_doc({
'doctype': 'Email Digest Recipient',
'parenttype': 'Email Digest',
'parentfield': 'recipients',
'parent': email_digest.name,
'recipient': recipient
})
doc.insert()

View File

@ -0,0 +1,9 @@
import frappe
def execute():
if frappe.db.exists('DocType', 'Member'):
frappe.reload_doc('Non Profit', 'doctype', 'Member')
if frappe.db.has_column('Member', 'subscription_activated'):
frappe.db.sql('UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1')
frappe.db.sql_ddl('ALTER table `tabMember` DROP COLUMN subscription_activated')

View File

@ -0,0 +1,9 @@
import frappe
def execute():
if frappe.db.has_table("Tax Withholding Category") \
and frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"):
frappe.db.sql("""
UPDATE `tabTax Withholding Category` set round_off_tax_amount = 0
WHERE round_off_tax_amount IS NULL
""")

View File

@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _, bold from frappe import _, bold
from frappe.utils import getdate, date_diff, comma_and, formatdate from frappe.utils import getdate, date_diff, comma_and, formatdate
from erpnext.hr.utils import validate_active_employee
class AdditionalSalary(Document): class AdditionalSalary(Document):
def on_submit(self): def on_submit(self):
@ -19,6 +20,7 @@ class AdditionalSalary(Document):
self.update_employee_referral(cancel=True) self.update_employee_referral(cancel=True)
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_dates() self.validate_dates()
self.validate_salary_structure() self.validate_salary_structure()
self.validate_recurring_additional_salary_overlap() self.validate_recurring_additional_salary_overlap()

View File

@ -9,10 +9,11 @@ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor 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.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 from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount, validate_active_employee
class EmployeeBenefitApplication(Document): class EmployeeBenefitApplication(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_duplicate_on_payroll_period() self.validate_duplicate_on_payroll_period()
if not self.max_benefits: if not self.max_benefits:
self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period) self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period)

View File

@ -8,12 +8,13 @@ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.payroll.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.utils import get_previous_claimed_amount, validate_active_employee
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period 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 from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
class EmployeeBenefitClaim(Document): class EmployeeBenefitClaim(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
max_benefits = get_max_benefits(self.employee, self.claim_date) max_benefits = get_max_benefits(self.employee, self.claim_date)
if not max_benefits or max_benefits <= 0: if not max_benefits or max_benefits <= 0:
frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee)) frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee))

View File

@ -6,9 +6,11 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_active_employee
class EmployeeIncentive(Document): class EmployeeIncentive(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_salary_structure() self.validate_salary_structure()
def validate_salary_structure(self): def validate_salary_structure(self):

View File

@ -8,11 +8,12 @@ from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \
calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period
class EmployeeTaxExemptionDeclaration(Document): class EmployeeTaxExemptionDeclaration(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_tax_declaration(self.declarations) validate_tax_declaration(self.declarations)
validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee)
self.set_total_declared_amount() self.set_total_declared_amount()

View File

@ -7,11 +7,12 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \
calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period
class EmployeeTaxExemptionProofSubmission(Document): class EmployeeTaxExemptionProofSubmission(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_tax_declaration(self.tax_exemption_proofs) validate_tax_declaration(self.tax_exemption_proofs)
self.set_total_actual_amount() self.set_total_actual_amount()
self.set_total_exemption_amount() self.set_total_exemption_amount()

View File

@ -7,11 +7,10 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import getdate from frappe.utils import getdate
from erpnext.hr.utils import validate_active_employee
class RetentionBonus(Document): class RetentionBonus(Document):
def validate(self): def validate(self):
if frappe.get_value('Employee', self.employee, 'status') != 'Active': validate_active_employee(self.employee)
frappe.throw(_('Cannot create Retention Bonus for Left or Inactive Employees'))
if getdate(self.bonus_payment_date) < getdate(): if getdate(self.bonus_payment_date) < getdate():
frappe.throw(_('Bonus Payment Date cannot be a past date')) frappe.throw(_('Bonus Payment Date cannot be a past date'))

View File

@ -4,11 +4,18 @@
frappe.ui.form.on('Salary Component', { frappe.ui.form.on('Salary Component', {
setup: function(frm) { setup: function(frm) {
frm.set_query("account", "accounts", function(doc, cdt, cdn) { frm.set_query("account", "accounts", function(doc, cdt, cdn) {
var d = locals[cdt][cdn]; let d = frappe.get_doc(cdt, cdn);
let root_type = "Liability";
if (frm.doc.type == "Deduction") {
root_type = "Expense";
}
return { return {
filters: { filters: {
"is_group": 0, "is_group": 0,
"company": d.company "company": d.company,
"root_type": root_type
} }
}; };
}); });

View File

@ -19,6 +19,7 @@ from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_appli
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits 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 from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.utils import validate_active_employee
from six import iteritems from six import iteritems
class SalarySlip(TransactionBase): class SalarySlip(TransactionBase):
@ -39,6 +40,7 @@ class SalarySlip(TransactionBase):
def validate(self): def validate(self):
self.status = self.get_status() self.status = self.get_status()
validate_active_employee(self.employee)
self.validate_dates() self.validate_dates()
self.check_existing() self.check_existing()
if not self.salary_slip_based_on_timesheet: if not self.salary_slip_based_on_timesheet:
@ -1091,6 +1093,7 @@ class SalarySlip(TransactionBase):
"applicant": self.employee, "applicant": self.employee,
"docstatus": 1, "docstatus": 1,
"repay_from_salary": 1, "repay_from_salary": 1,
"company": self.company
}) })
def make_loan_repayment_entry(self): def make_loan_repayment_entry(self):

View File

@ -482,14 +482,19 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
employee = frappe.db.get_value("Employee", {"user_id": user}) employee = frappe.db.get_value("Employee",
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) {
"user_id": user
},
["name", "company", "employee_name"],
as_dict=True)
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
if not salary_slip_name: if not salary_slip_name:
salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee) salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee.name)
salary_slip.employee_name = frappe.get_value("Employee", salary_slip.employee_name = employee.employee_name
{"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name")
salary_slip.payroll_frequency = payroll_frequency salary_slip.payroll_frequency = payroll_frequency
salary_slip.posting_date = nowdate() salary_slip.posting_date = nowdate()
salary_slip.insert() salary_slip.insert()

View File

@ -119,26 +119,25 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None,
if test_tax: if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
if not frappe.db.exists('Salary Structure', salary_structure): if frappe.db.exists("Salary Structure", salary_structure):
details = { frappe.db.delete("Salary Structure", salary_structure)
"doctype": "Salary Structure",
"name": salary_structure,
"company": company or erpnext.get_default_company(),
"earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"payroll_frequency": payroll_frequency,
"payment_account": get_random("Account", filters={'account_currency': currency}),
"currency": currency
}
if other_details and isinstance(other_details, dict):
details.update(other_details)
salary_structure_doc = frappe.get_doc(details)
salary_structure_doc.insert()
if not dont_submit:
salary_structure_doc.submit()
else: details = {
salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure) "doctype": "Salary Structure",
"name": salary_structure,
"company": company or erpnext.get_default_company(),
"earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"payroll_frequency": payroll_frequency,
"payment_account": get_random("Account", filters={'account_currency': currency}),
"currency": currency
}
if other_details and isinstance(other_details, dict):
details.update(other_details)
salary_structure_doc = frappe.get_doc(details)
salary_structure_doc.insert()
if not dont_submit:
salary_structure_doc.submit()
filters = {'employee':employee, 'docstatus': 1} filters = {'employee':employee, 'docstatus': 1}
if not from_date and payroll_period: if not from_date and payroll_period:

View File

@ -95,6 +95,7 @@ def execute(filters=None):
"amount": salary.net_pay, "amount": salary.net_pay,
} }
data.append(row) data.append(row)
return columns, data return columns, data
def get_bank_accounts(): def get_bank_accounts():
@ -116,7 +117,7 @@ def get_payroll_entries(accounts, filters):
entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"]) entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"])
payment_accounts = [d.payment_account for d in entries] payment_accounts = [d.payment_account for d in entries]
set_company_account(payment_accounts, entries) entries = set_company_account(payment_accounts, entries)
return entries return entries
def get_salary_slips(payroll_entries): def get_salary_slips(payroll_entries):

View File

@ -2,6 +2,7 @@ import frappe
from frappe.utils import cint from frappe.utils import cint
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
from erpnext.shopping_cart.product_info import get_product_info_for_website from erpnext.shopping_cart.product_info import get_product_info_for_website
from erpnext.setup.doctype.item_group.item_group import get_child_groups
def get_field_filter_data(): def get_field_filter_data():
product_settings = get_product_settings() product_settings = get_product_settings()
@ -89,6 +90,7 @@ def get_products_for_website(field_filters=None, attribute_filters=None, search=
def get_products_html_for_website(field_filters=None, attribute_filters=None): def get_products_html_for_website(field_filters=None, attribute_filters=None):
field_filters = frappe.parse_json(field_filters) field_filters = frappe.parse_json(field_filters)
attribute_filters = frappe.parse_json(attribute_filters) attribute_filters = frappe.parse_json(attribute_filters)
set_item_group_filters(field_filters)
items = get_products_for_website(field_filters, attribute_filters) items = get_products_for_website(field_filters, attribute_filters)
html = ''.join(get_html_for_items(items)) html = ''.join(get_html_for_items(items))
@ -98,6 +100,10 @@ def get_products_html_for_website(field_filters=None, attribute_filters=None):
return html return html
def set_item_group_filters(field_filters):
if field_filters is not None and 'item_group' in field_filters:
field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
def get_item_codes_by_attributes(attribute_filters, template_item_code=None): def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
items = [] items = []

View File

@ -15,12 +15,15 @@ from erpnext.manufacturing.doctype.workstation.workstation import (check_if_with
WorkstationHolidayError) WorkstationHolidayError)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
from erpnext.hr.utils import validate_active_employee
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class OverWorkLoggedError(frappe.ValidationError): pass class OverWorkLoggedError(frappe.ValidationError): pass
class Timesheet(Document): class Timesheet(Document):
def validate(self): def validate(self):
if self.employee:
validate_active_employee(self.employee)
self.set_employee_name() self.set_employee_name()
self.set_status() self.set_status()
self.validate_dates() self.validate_dates()

Some files were not shown because too many files have changed in this diff Show More