Merge branch 'version-13' of https://github.com/frappe/erpnext into enterprise-hotfix

This commit is contained in:
Deepesh Garg
2021-08-12 17:46:44 +05:30
205 changed files with 4795 additions and 2903 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,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
erpnext/education/ @ruchamahabal
erpnext/healthcare/ @ruchamahabal
erpnext/hr/ @ruchamahabal
erpnext/non_profit/ @ruchamahabal
erpnext/payroll @ruchamahabal
erpnext/projects/ @ruchamahabal
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
.github/ @surajshetty3416 @ankush
requirements.txt @gavindsouza

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.6.0' __version__ = '13.8.0'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@@ -301,17 +301,21 @@ def process_deferred_accounting(posting_date=None):
start_date = add_months(today(), -1) start_date = add_months(today(), -1)
end_date = add_days(today(), -1) end_date = add_days(today(), -1)
for record_type in ('Income', 'Expense'): companies = frappe.get_all('Company')
doc = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=posting_date,
start_date=start_date,
end_date=end_date,
type=record_type
))
doc.insert() for company in companies:
doc.submit() for record_type in ('Income', 'Expense'):
doc = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
company=company.name,
posting_date=posting_date,
start_date=start_date,
end_date=end_date,
type=record_type
))
doc.insert()
doc.submit()
def make_gl_entries(doc, credit_account, debit_account, against, def make_gl_entries(doc, credit_account, debit_account, against,
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None): amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):

View File

@@ -51,7 +51,7 @@ class BankStatementImport(DataImport):
self.import_file, self.google_sheets_url self.import_file, self.google_sheets_url
) )
if 'Bank Account' not in json.dumps(preview): if 'Bank Account' not in json.dumps(preview['columns']):
frappe.throw(_("Please add the Bank Account column")) frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info from frappe.core.page.background_jobs.background_jobs import get_info

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

@@ -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)
@@ -1318,9 +1360,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
return frappe._dict({ return frappe._dict({
"due_date": ref_doc.get("due_date"), "due_date": ref_doc.get("due_date"),
"total_amount": total_amount, "total_amount": flt(total_amount),
"outstanding_amount": outstanding_amount, "outstanding_amount": flt(outstanding_amount),
"exchange_rate": exchange_rate, "exchange_rate": flt(exchange_rate),
"bill_no": bill_no "bill_no": bill_no
}) })
@@ -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

@@ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase):
party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center) party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center)
self.assertEqual(pe.cost_center, si.cost_center) self.assertEqual(pe.cost_center, si.cost_center)
self.assertEqual(expected_account_balance, account_balance) self.assertEqual(flt(expected_account_balance), account_balance)
self.assertEqual(expected_party_balance, party_balance) self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(expected_party_account_balance, party_account_balance) self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def create_payment_terms_template(): def create_payment_terms_template():

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

@@ -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

@@ -207,10 +207,9 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
@frappe.whitelist() @frappe.whitelist()
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True): def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
billing_email = frappe.db.sql(""" billing_email = frappe.db.sql("""
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \ SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \ WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
c.is_billing_contact=1 \ order by c.creation desc""", customer_name)
order by c.creation desc""")
if len(billing_email) == 0 or (billing_email[0][0] is None): if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary: if billing_and_primary:

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
@@ -1010,21 +1124,21 @@ class TestPurchaseInvoice(unittest.TestCase):
# Check GLE for Purchase Invoice # Check GLE for Purchase Invoice
# Zero net effect on final TDS Payable on invoice # Zero net effect on final TDS Payable on invoice
expected_gle = [ expected_gle = [
['_Test Account Cost for Goods Sold - _TC', 30000, 0], ['_Test Account Cost for Goods Sold - _TC', 30000],
['_Test Account Excise Duty - _TC', 0, 3000], ['_Test Account Excise Duty - _TC', -3000],
['Creditors - _TC', 0, 27000], ['Creditors - _TC', -27000],
['TDS Payable - _TC', 3000, 3000] ['TDS Payable - _TC', 0]
] ]
gl_entries = frappe.db.sql("""select account, debit, credit gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
from `tabGL Entry` from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s where voucher_type='Purchase Invoice' and voucher_no=%s
group by account
order by account asc""", (purchase_invoice.name), as_dict=1) order by account asc""", (purchase_invoice.name), as_dict=1)
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.debit) self.assertEqual(expected_gle[i][1], gle.amount)
self.assertEqual(expected_gle[i][2], gle.credit)
def update_tax_witholding_category(company, account, date): def update_tax_witholding_category(company, account, date):
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year

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

@@ -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

@@ -1957,6 +1957,33 @@ class TestSalesInvoice(unittest.TestCase):
einvoice = make_einvoice(si) einvoice = make_einvoice(si)
validate_totals(einvoice) validate_totals(einvoice)
def test_item_tax_net_range(self):
item = create_item("T Shirt")
item.set('taxes', [])
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500
})
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000
})
item.save()
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
# Apply discount
sales_invoice.apply_discount_on = 'Net Total'
sales_invoice.discount_amount = 300
sales_invoice.save()
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
def get_sales_invoice_for_e_invoice(): def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'
@@ -1985,32 +2012,6 @@ def get_sales_invoice_for_e_invoice():
return si return si
def test_item_tax_net_range(self):
item = create_item("T Shirt")
item.set('taxes', [])
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500
})
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000
})
item.save()
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
# Apply discount
sales_invoice.apply_discount_on = 'Net Total'
sales_invoice.discount_amount = 300
sales_invoice.save()
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
def make_test_address_for_ewaybill(): def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
@@ -2087,9 +2088,9 @@ def make_sales_invoice_for_ewaybill():
if not gst_account: if not gst_account:
gst_settings.append("gst_accounts", { gst_settings.append("gst_accounts", {
"company": "_Test Company", "company": "_Test Company",
"cgst_account": "CGST - _TC", "cgst_account": "Output Tax CGST - _TC",
"sgst_account": "SGST - _TC", "sgst_account": "Output Tax SGST - _TC",
"igst_account": "IGST - _TC", "igst_account": "Output Tax IGST - _TC",
}) })
gst_settings.save() gst_settings.save()
@@ -2106,7 +2107,7 @@ def make_sales_invoice_for_ewaybill():
si.append("taxes", { si.append("taxes", {
"charge_type": "On Net Total", "charge_type": "On Net Total",
"account_head": "CGST - _TC", "account_head": "Output Tax CGST - _TC",
"cost_center": "Main - _TC", "cost_center": "Main - _TC",
"description": "CGST @ 9.0", "description": "CGST @ 9.0",
"rate": 9 "rate": 9
@@ -2114,7 +2115,7 @@ def make_sales_invoice_for_ewaybill():
si.append("taxes", { si.append("taxes", {
"charge_type": "On Net Total", "charge_type": "On Net Total",
"account_head": "SGST - _TC", "account_head": "Output Tax SGST - _TC",
"cost_center": "Main - _TC", "cost_center": "Main - _TC",
"description": "SGST @ 9.0", "description": "SGST @ 9.0",
"rate": 9 "rate": 9

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,24 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
cur_frm.add_fetch("customer", "customer_group", "customer_group" );
cur_frm.add_fetch("supplier", "supplier_group_name", "supplier_group" );
frappe.ui.form.on("Tax Rule", "tax_type", function(frm) {
frm.toggle_reqd("sales_tax_template", frm.doc.tax_type=="Sales");
frm.toggle_reqd("purchase_tax_template", frm.doc.tax_type=="Purchase");
})
frappe.ui.form.on("Tax Rule", "onload", function(frm) {
if(frm.doc.__islocal) {
frm.set_value("use_for_shopping_cart", 1);
}
})
frappe.ui.form.on("Tax Rule", "refresh", function(frm) {
frappe.ui.form.trigger("Tax Rule", "tax_type");
})
frappe.ui.form.on("Tax Rule", "customer", function(frm) { frappe.ui.form.on("Tax Rule", "customer", function(frm) {
if(frm.doc.customer) { if(frm.doc.customer) {
frappe.call({ frappe.call({

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,7 @@ class TestTaxRule(unittest.TestCase):
tax_rule1 = make_tax_rule(customer_group= "All Customer Groups", tax_rule1 = make_tax_rule(customer_group= "All Customer Groups",
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01") sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
tax_rule1.save() tax_rule1.save()
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":0}), self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}),
"_Test Sales Taxes and Charges Template - _TC") "_Test Sales Taxes and Charges Template - _TC")
def test_conflict_with_overlapping_dates(self): def test_conflict_with_overlapping_dates(self):

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

@@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency) select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry` from `tabGL Entry`
where party_type = %s and party=%s where party_type = %s and party=%s
and is_cancelled = 0
group by company""", (party_type, party))) group by company""", (party_type, party)))
for d in companies: for d in companies:

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

@@ -397,6 +397,7 @@ def get_chart_data(filters, columns, data):
{'name': 'Budget', 'chartType': 'bar', 'values': budget_values}, {'name': 'Budget', 'chartType': 'bar', 'values': budget_values},
{'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values} {'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values}
] ]
} },
'type' : 'bar'
} }

View File

@@ -380,7 +380,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency, gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
acc.account_name, acc.account_number acc.account_name, acc.account_number
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s {additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions), order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
{ {

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

@@ -168,21 +168,24 @@ def get_columns(filters):
"label": _("Income"), "label": _("Income"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 305
}, },
{ {
"fieldname": "expense", "fieldname": "expense",
"label": _("Expense"), "label": _("Expense"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 305
}, },
{ {
"fieldname": "gross_profit_loss", "fieldname": "gross_profit_loss",
"label": _("Gross Profit / Loss"), "label": _("Gross Profit / Loss"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 120 "width": 307
} }
] ]

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)
@@ -784,7 +788,7 @@ def get_children(doctype, parent, company, is_root=False):
return acc return acc
def create_payment_gateway_account(gateway, payment_channel="Email"): def create_payment_gateway_account(gateway, payment_channel="Email"):
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
company = frappe.db.get_value("Global Defaults", None, "default_company") company = frappe.db.get_value("Global Defaults", None, "default_company")
if not company: if not company:
@@ -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

@@ -97,6 +97,9 @@
"is_fixed_asset", "is_fixed_asset",
"item_tax_rate", "item_tax_rate",
"section_break_72", "section_break_72",
"production_plan",
"production_plan_item",
"production_plan_sub_assembly_item",
"page_break" "page_break"
], ],
"fields": [ "fields": [
@@ -803,13 +806,37 @@
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "production_plan",
"fieldtype": "Link",
"label": "Production Plan",
"options": "Production Plan",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "production_plan_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Production Plan Item",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "production_plan_sub_assembly_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Production Plan Sub Assembly Item",
"no_copy": 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-03-22 11:46:12.357435", "modified": "2021-06-28 19:22:22.715365",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@@ -60,10 +60,23 @@ frappe.ui.form.on("Supplier", {
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
}, __('Create')); }, __('Create'));
frm.add_custom_button(__('Get Supplier Group Details'), function () {
frm.trigger("get_supplier_group_details");
}, __('Actions'));
// indicators // indicators
erpnext.utils.set_party_dashboard_indicators(frm); erpnext.utils.set_party_dashboard_indicators(frm);
} }
}, },
get_supplier_group_details: function(frm) {
frappe.call({
method: "get_supplier_group_details",
doc: frm.doc,
callback: function() {
frm.refresh();
}
});
},
is_internal_supplier: function(frm) { is_internal_supplier: function(frm) {
if (frm.doc.is_internal_supplier == 1) { if (frm.doc.is_internal_supplier == 1) {

View File

@@ -51,6 +51,23 @@ class Supplier(TransactionBase):
validate_party_accounts(self) validate_party_accounts(self)
self.validate_internal_supplier() self.validate_internal_supplier()
@frappe.whitelist()
def get_supplier_group_details(self):
doc = frappe.get_doc('Supplier Group', self.supplier_group)
self.payment_terms = ""
self.accounts = []
if doc.accounts:
for account in doc.accounts:
child = self.append('accounts')
child.company = account.company
child.account = account.account
if doc.payment_terms:
self.payment_terms = doc.payment_terms
self.save()
def validate_internal_supplier(self): def validate_internal_supplier(self):
internal_supplier = frappe.db.get_value("Supplier", internal_supplier = frappe.db.get_value("Supplier",
{"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name")
@@ -86,4 +103,4 @@ class Supplier(TransactionBase):
create_contact(supplier, 'Supplier', create_contact(supplier, 'Supplier',
doc.name, args.get('supplier_email_' + str(i))) doc.name, args.get('supplier_email_' + str(i)))
except frappe.NameError: except frappe.NameError:
pass pass

View File

@@ -13,6 +13,30 @@ test_records = frappe.get_test_records('Supplier')
class TestSupplier(unittest.TestCase): class TestSupplier(unittest.TestCase):
def test_get_supplier_group_details(self):
doc = frappe.new_doc("Supplier Group")
doc.supplier_group_name = "_Testing Supplier Group"
doc.payment_terms = "_Test Payment Term Template 3"
doc.accounts = []
test_account_details = {
"company": "_Test Company",
"account": "Creditors - _TC",
}
doc.append("accounts", test_account_details)
doc.save()
s_doc = frappe.new_doc("Supplier")
s_doc.supplier_name = "Testing Supplier"
s_doc.supplier_group = "_Testing Supplier Group"
s_doc.payment_terms = ""
s_doc.accounts = []
s_doc.insert()
s_doc.get_supplier_group_details()
self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
self.assertEqual(s_doc.accounts[0].company, "_Test Company")
self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
s_doc.delete()
doc.delete()
def test_supplier_default_payment_terms(self): def test_supplier_default_payment_terms(self):
# Payment Term based on Days after invoice date # Payment Term based on Days after invoice date
frappe.db.set_value( frappe.db.set_value(
@@ -136,4 +160,4 @@ def create_supplier(**args):
return doc return doc
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
return frappe.get_doc("Supplier", args.supplier_name) return frappe.get_doc("Supplier", args.supplier_name)

View File

@@ -0,0 +1,69 @@
# Version 13.7.0 Release Notes
### Features & Enhancements
- Optionally allow rejected quality inspection on submission ([#26133](https://github.com/frappe/erpnext/pull/26133))
- Bootstrapped GST Setup for India ([#25415](https://github.com/frappe/erpnext/pull/25415))
- Fetching details from supplier/customer groups ([#26454](https://github.com/frappe/erpnext/pull/26454))
- Provision to make subcontracted purchase order from the production plan ([#26240](https://github.com/frappe/erpnext/pull/26240))
- Optimized code for reposting item valuation ([#26432](https://github.com/frappe/erpnext/pull/26432))
### Fixes
- Auto process deferred accounting for multi-company setup ([#26277](https://github.com/frappe/erpnext/pull/26277))
- Error while fetching item taxes ([#26218](https://github.com/frappe/erpnext/pull/26218))
- Validation check for batch for stock reconciliation type in stock entry(bp #26370 ) ([#26488](https://github.com/frappe/erpnext/pull/26488))
- Error popup for COA errors ([#26358](https://github.com/frappe/erpnext/pull/26358))
- Precision for expected values in payment entry test ([#26394](https://github.com/frappe/erpnext/pull/26394))
- Bank statement import ([#26287](https://github.com/frappe/erpnext/pull/26287))
- LMS progress issue ([#26253](https://github.com/frappe/erpnext/pull/26253))
- Paging buttons not working on item group portal page ([#26497](https://github.com/frappe/erpnext/pull/26497))
- Omit item discount amount for e-invoicing ([#26353](https://github.com/frappe/erpnext/pull/26353))
- Validate LCV for Invoices without Update Stock ([#26333](https://github.com/frappe/erpnext/pull/26333))
- Remove cancelled entries in consolidated financial statements ([#26331](https://github.com/frappe/erpnext/pull/26331))
- Fetching employee in payroll entry ([#26271](https://github.com/frappe/erpnext/pull/26271))
- To fetch the correct field in Tax Rule ([#25927](https://github.com/frappe/erpnext/pull/25927))
- Order and time of operations in multilevel BOM work order ([#25886](https://github.com/frappe/erpnext/pull/25886))
- Fixed Budget Variance Graph color from all black to default ([#26368](https://github.com/frappe/erpnext/pull/26368))
- TDS computation summary shows cancelled invoices (#26456) ([#26486](https://github.com/frappe/erpnext/pull/26486))
- Do not consider cancelled entries in party dashboard ([#26231](https://github.com/frappe/erpnext/pull/26231))
- Add validation for 'for_qty' else throws errors ([#25829](https://github.com/frappe/erpnext/pull/25829))
- Move the rename abbreviation job to long queue (#26434) ([#26462](https://github.com/frappe/erpnext/pull/26462))
- Query for Training Event ([#26388](https://github.com/frappe/erpnext/pull/26388))
- Item group portal issues (backport) ([#26493](https://github.com/frappe/erpnext/pull/26493))
- When lead is created with mobile_no, mobile_no value gets lost ([#26298](https://github.com/frappe/erpnext/pull/26298))
- WIP needs to be set before submit on skip_transfer (bp #26499) ([#26507](https://github.com/frappe/erpnext/pull/26507))
- Incorrect valuation rate in stock reconciliation ([#26259](https://github.com/frappe/erpnext/pull/26259))
- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
- Changed profitability analysis report width ([#26165](https://github.com/frappe/erpnext/pull/26165))
- Unable to download GSTR-1 json ([#26468](https://github.com/frappe/erpnext/pull/26468))
- Unallocated amount in Payment Entry after taxes ([#26472](https://github.com/frappe/erpnext/pull/26472))
- Include Stock Reco logic in `update_qty_in_future_sle` ([#26158](https://github.com/frappe/erpnext/pull/26158))
- Update cost not working in the draft BOM ([#26279](https://github.com/frappe/erpnext/pull/26279))
- Cancellation of Loan Security Pledges ([#26252](https://github.com/frappe/erpnext/pull/26252))
- fix(e-invoicing): allow export invoice even if no taxes applied (#26363) ([#26405](https://github.com/frappe/erpnext/pull/26405))
- Delete accounts (an empty file) ([#25323](https://github.com/frappe/erpnext/pull/25323))
- Errors on parallel requests creation of company for India ([#26470](https://github.com/frappe/erpnext/pull/26470))
- Incorrect bom no added for non-variant items on variant boms ([#26320](https://github.com/frappe/erpnext/pull/26320))
- Incorrect discount amount on amended document ([#26466](https://github.com/frappe/erpnext/pull/26466))
- Added a message to enable appointment booking if disabled ([#26334](https://github.com/frappe/erpnext/pull/26334))
- fix(pos): taxes amount in pos item cart ([#26411](https://github.com/frappe/erpnext/pull/26411))
- Track changes on batch ([#26382](https://github.com/frappe/erpnext/pull/26382))
- Stock entry with putaway rule not working ([#26350](https://github.com/frappe/erpnext/pull/26350))
- Only "Tax" type accounts should be shown for selection in GST Settings ([#26300](https://github.com/frappe/erpnext/pull/26300))
- Added permission for employee to book appointment ([#26255](https://github.com/frappe/erpnext/pull/26255))
- Allow to make job card without employee ([#26312](https://github.com/frappe/erpnext/pull/26312))
- Project Portal Enhancements ([#26290](https://github.com/frappe/erpnext/pull/26290))
- BOM stock report not working ([#26332](https://github.com/frappe/erpnext/pull/26332))
- Order Items by weightage in the web items query ([#26284](https://github.com/frappe/erpnext/pull/26284))
- Removed values out of sync validation from stock transactions ([#26226](https://github.com/frappe/erpnext/pull/26226))
- Payroll-entry minor fix ([#26349](https://github.com/frappe/erpnext/pull/26349))
- Allow user to change the To Date in the blanket order even after submit of order ([#26241](https://github.com/frappe/erpnext/pull/26241))
- Value fetching for custom field in POS ([#26367](https://github.com/frappe/erpnext/pull/26367))
- Iteration through accounts only when accounts exist ([#26391](https://github.com/frappe/erpnext/pull/26391))
- Employee Inactive status implications ([#26244](https://github.com/frappe/erpnext/pull/26244))
- Multi-currency issue ([#26458](https://github.com/frappe/erpnext/pull/26458))
- FG item not fetched in manufacture entry ([#26509](https://github.com/frappe/erpnext/pull/26509))
- Set query for training events ([#26303](https://github.com/frappe/erpnext/pull/26303))
- Fetch batch items in stock reconciliation ([#26213](https://github.com/frappe/erpnext/pull/26213))
- Employee selection not working in payroll entry ([#26278](https://github.com/frappe/erpnext/pull/26278))
- POS item cart dom updates (#26459) ([#26461](https://github.com/frappe/erpnext/pull/26461))
- dunning calculation of grand total when rate of interest is 0% ([#26285](https://github.com/frappe/erpnext/pull/26285))

View File

@@ -0,0 +1,39 @@
# Version 13.8.0 Release Notes
### Features & Enhancements
- Report to show COGS by item groups ([#26222](https://github.com/frappe/erpnext/pull/26222))
- Enhancements in TDS ([#26677](https://github.com/frappe/erpnext/pull/26677))
- API Endpoint to update halted Razorpay subscriptions ([#26564](https://github.com/frappe/erpnext/pull/26564))
### Fixes
- Incorrect bom name ([#26600](https://github.com/frappe/erpnext/pull/26600))
- Exchange rate revaluation posting date and precision fixes ([#26651](https://github.com/frappe/erpnext/pull/26651))
- POS item cart dom updates ([#26460](https://github.com/frappe/erpnext/pull/26460))
- General Ledger report not working with filter group by ([#26439](https://github.com/frappe/erpnext/pull/26438))
- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206))
- Validation check for batch for stock reconciliation type in stock entry ([#26487](https://github.com/frappe/erpnext/pull/26487))
- Improved UX for additional discount field ([#26502](https://github.com/frappe/erpnext/pull/26502))
- Add missing cess amount in GSTR-3B report ([#26644](https://github.com/frappe/erpnext/pull/26644))
- Optimized code for reposting item valuation ([#26431](https://github.com/frappe/erpnext/pull/26431))
- FG item not fetched in manufacture entry ([#26508](https://github.com/frappe/erpnext/pull/26508))
- Errors on parallel requests creation of company for India ([#26420](https://github.com/frappe/erpnext/pull/26420))
- Incorrect valuation rate calculation in gross profit report ([#26558](https://github.com/frappe/erpnext/pull/26558))
- Empty "against account" in Purchase Receipt GLE ([#26712](https://github.com/frappe/erpnext/pull/26712))
- Remove cancelled entries from Stock and Account Value comparison report ([#26721](https://github.com/frappe/erpnext/pull/26721))
- Remove manual permission checking ([#26691](https://github.com/frappe/erpnext/pull/26691))
- Delete child docs when parent doc is deleted ([#26518](https://github.com/frappe/erpnext/pull/26518))
- GST Reports timeout issue ([#26646](https://github.com/frappe/erpnext/pull/26646))
- Parent condition in pricing rules ([#26727](https://github.com/frappe/erpnext/pull/26727))
- Added Company filters for Loan ([#26294](https://github.com/frappe/erpnext/pull/26294))
- Incorrect discount amount on amended document ([#26292](https://github.com/frappe/erpnext/pull/26292))
- Exchange gain loss not set for advances linked with invoices ([#26436](https://github.com/frappe/erpnext/pull/26436))
- Unallocated amount in Payment Entry after taxes ([#26412](https://github.com/frappe/erpnext/pull/26412))
- Wrong operation time in Work Order ([#26613](https://github.com/frappe/erpnext/pull/26613))
- Serial No and Batch validation ([#26614](https://github.com/frappe/erpnext/pull/26614))
- Gl Entries for exchange gain loss ([#26734](https://github.com/frappe/erpnext/pull/26734))
- TDS computation summary shows cancelled invoices ([#26485](https://github.com/frappe/erpnext/pull/26485))
- Price List rate not fetched for return sales invoice fixed ([#26560](https://github.com/frappe/erpnext/pull/26560))
- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576))
- Ignore mandatory fields while creating payment reconciliation Journal Entry ([#26643](https://github.com/frappe/erpnext/pull/26643))
- Unable to download GSTR-1 json ([#26418](https://github.com/frappe/erpnext/pull/26418))
- Paging buttons not working on item group portal page ([#26498](https://github.com/frappe/erpnext/pull/26498))

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

@@ -99,9 +99,10 @@ def validate_returned_items(doc):
frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}") frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}")
.format(d.idx, s, doc.doctype, doc.return_against)) .format(d.idx, s, doc.doctype, doc.return_against))
if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \ if (warehouse_mandatory and not d.get("warehouse") and
and not d.get("warehouse"): frappe.db.get_value("Item", d.item_code, "is_stock_item")
frappe.throw(_("Warehouse is mandatory")) ):
frappe.throw(_("Warehouse is mandatory"))
items_returned = True items_returned = True
@@ -462,4 +463,4 @@ def get_returned_serial_nos(child_doc, parent_doc):
for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters): for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters):
serial_nos.extend(get_serial_nos(row.serial_no)) serial_nos.extend(get_serial_nos(row.serial_no))
return serial_nos return serial_nos

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

@@ -102,7 +102,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-01-28 16:16:45.447213", "modified": "2021-06-29 18:27:02.832979",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Appointment", "name": "Appointment",
@@ -153,6 +153,18 @@
"role": "Sales User", "role": "Sales User",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1,
"write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,

View File

@@ -168,12 +168,13 @@ class Lead(SellingController):
if self.phone: if self.phone:
contact.append("phone_nos", { contact.append("phone_nos", {
"phone": self.phone, "phone": self.phone,
"is_primary": 1 "is_primary_phone": 1
}) })
if self.mobile_no: if self.mobile_no:
contact.append("phone_nos", { contact.append("phone_nos", {
"phone": self.mobile_no "phone": self.mobile_no,
"is_primary_mobile_no":1
}) })
contact.insert(ignore_permissions=True) contact.insert(ignore_permissions=True)

View File

@@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program):
student = get_current_student() student = get_current_student()
course_enrollment = get_enrollment("course", course, student.name) course_enrollment = get_enrollment("course", course, student.name)
if not course_enrollment: if not course_enrollment:
program_enrollment = get_enrollment('program', program, student.name) program_enrollment = get_enrollment('program', program.name, student.name)
if not program_enrollment: if not program_enrollment:
frappe.throw(_("You are not enrolled in program {0}").format(program)) frappe.throw(_("You are not enrolled in program {0}").format(program))
return return
return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name)) return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name))
else: else:
return frappe.get_doc('Course Enrollment', course_enrollment) return frappe.get_doc('Course Enrollment', course_enrollment)

View File

@@ -7,16 +7,21 @@ import frappe
import unittest import unittest
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.erpnext_integrations.utils import create_mode_of_payment
class TestMpesaSettings(unittest.TestCase): class TestMpesaSettings(unittest.TestCase):
def setUp(self):
# create payment gateway in setup
create_mpesa_settings(payment_gateway_name="_Test")
create_mpesa_settings(payment_gateway_name="_Account Balance")
create_mpesa_settings(payment_gateway_name="Payment")
def tearDown(self): def tearDown(self):
frappe.db.sql('delete from `tabMpesa Settings`') frappe.db.sql('delete from `tabMpesa Settings`')
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"') frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
def test_creation_of_payment_gateway(self): def test_creation_of_payment_gateway(self):
create_mpesa_settings(payment_gateway_name="_Test") mode_of_payment = create_mode_of_payment('Mpesa-_Test', payment_type="Phone")
mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test")
self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"})) self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"}))
self.assertTrue(mode_of_payment.name) self.assertTrue(mode_of_payment.name)
self.assertEqual(mode_of_payment.type, "Phone") self.assertEqual(mode_of_payment.type, "Phone")
@@ -47,7 +52,6 @@ class TestMpesaSettings(unittest.TestCase):
integration_request.delete() integration_request.delete()
def test_processing_of_callback_payload(self): def test_processing_of_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
@@ -90,7 +94,6 @@ class TestMpesaSettings(unittest.TestCase):
pos_invoice.delete() pos_invoice.delete()
def test_processing_of_multiple_callback_payload(self): def test_processing_of_multiple_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
@@ -141,7 +144,6 @@ class TestMpesaSettings(unittest.TestCase):
pos_invoice.delete() pos_invoice.delete()
def test_processing_of_only_one_succes_callback_payload(self): def test_processing_of_only_one_succes_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
@@ -202,6 +204,7 @@ def create_mpesa_settings(payment_gateway_name="Express"):
doc = frappe.get_doc(dict( #nosec doc = frappe.get_doc(dict( #nosec
doctype="Mpesa Settings", doctype="Mpesa Settings",
sandbox=1,
payment_gateway_name=payment_gateway_name, payment_gateway_name=payment_gateway_name,
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn", consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
consumer_secret="VI1oS3oBGPJfh3JyvLHw", consumer_secret="VI1oS3oBGPJfh3JyvLHw",

View File

@@ -52,7 +52,8 @@ def create_mode_of_payment(gateway, payment_type="General"):
"payment_gateway": gateway "payment_gateway": gateway
}, ['payment_account']) }, ['payment_account'])
if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account: mode_of_payment = frappe.db.exists("Mode of Payment", gateway)
if not mode_of_payment and payment_gateway_account:
mode_of_payment = frappe.get_doc({ mode_of_payment = frappe.get_doc({
"doctype": "Mode of Payment", "doctype": "Mode of Payment",
"mode_of_payment": gateway, "mode_of_payment": gateway,
@@ -66,6 +67,10 @@ def create_mode_of_payment(gateway, payment_type="General"):
}) })
mode_of_payment.insert(ignore_permissions=True) mode_of_payment.insert(ignore_permissions=True)
return mode_of_payment
elif mode_of_payment:
return frappe.get_doc("Mode of Payment", mode_of_payment)
def get_tracking_url(carrier, tracking_number): def get_tracking_url(carrier, tracking_number):
# Return the formatted Tracking URL. # Return the formatted Tracking URL.
tracking_url = '' tracking_url = ''

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 = {
@@ -157,6 +158,7 @@ website_route_rules = [
"parents": [{"label": _("Material Request"), "route": "material-requests"}] "parents": [{"label": _("Material Request"), "route": "material-requests"}]
} }
}, },
{"from_route": "/project", "to_route": "Project"}
] ]
standard_portal_menu_items = [ standard_portal_menu_items = [

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,6 +19,7 @@ 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):
@@ -183,9 +185,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)

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()

View File

@@ -72,7 +72,8 @@ class TestExpenseClaim(unittest.TestCase):
def test_expense_claim_gl_entry(self): def test_expense_claim_gl_entry(self):
payable_account = get_payable_account(company_name) payable_account = get_payable_account(company_name)
taxes = generate_taxes() taxes = generate_taxes()
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", do_not_submit=True, taxes=taxes) expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4",
do_not_submit=True, taxes=taxes)
expense_claim.submit() expense_claim.submit()
gl_entries = frappe.db.sql("""select account, debit, credit gl_entries = frappe.db.sql("""select account, debit, credit
@@ -82,7 +83,7 @@ class TestExpenseClaim(unittest.TestCase):
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [ expected_values = dict((d[0], d) for d in [
['CGST - _TC4',18.0, 0.0], ['Output Tax CGST - _TC4',18.0, 0.0],
[payable_account, 0.0, 218.0], [payable_account, 0.0, 218.0],
["Travel Expenses - _TC4", 200.0, 0.0] ["Travel Expenses - _TC4", 200.0, 0.0]
]) ])
@@ -145,7 +146,7 @@ def generate_taxes():
parent_account = frappe.db.get_value('Account', parent_account = frappe.db.get_value('Account',
{'company': company_name, 'is_group':1, 'account_type': 'Tax'}, {'company': company_name, 'is_group':1, 'account_type': 'Tax'},
'name') 'name')
account = create_account(company=company_name, account_name="CGST", account_type="Tax", parent_account=parent_account) account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account)
return {'taxes':[{ return {'taxes':[{
"account_head": account, "account_head": account,
"rate": 0, "rate": 0,

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

@@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', {
frappe.set_route("List", "Training Feedback"); frappe.set_route("List", "Training Feedback");
}); });
} }
} frm.events.set_employee_query(frm);
}); },
frappe.ui.form.on("Training Event Employee", { set_employee_query: function(frm) {
employee: function (frm) {
let emp = []; let emp = [];
for (let d in frm.doc.employees) { for (let d in frm.doc.employees) {
if (frm.doc.employees[d].employee) { if (frm.doc.employees[d].employee) {
@@ -34,9 +33,17 @@ frappe.ui.form.on("Training Event Employee", {
frm.set_query("employee", "employees", function () { frm.set_query("employee", "employees", function () {
return { return {
filters: { filters: {
name: ["NOT IN", emp] name: ["NOT IN", emp],
status: "Active"
} }
}; };
}); });
} }
}); });
frappe.ui.form.on("Training Event Employee", {
employee: function(frm) {
frm.events.set_employee_query(frm);
}
});

View File

@@ -19,6 +19,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Employee", "label": "Employee",
"no_copy": 1,
"options": "Employee" "options": "Employee"
}, },
{ {
@@ -68,7 +69,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-05-21 12:41:59.336237", "modified": "2021-07-02 17:20:27.630176",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Training Event Employee", "name": "Training Event Employee",

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

@@ -35,7 +35,9 @@
"no_copy": 1, "no_copy": 1,
"options": "Loan Security Pledge", "options": "Loan Security Pledge",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fetch_from": "loan_application.applicant", "fetch_from": "loan_application.applicant",
@@ -45,47 +47,63 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Applicant", "label": "Applicant",
"options": "applicant_type", "options": "applicant_type",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "loan_security_details_section", "fieldname": "loan_security_details_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Loan Security Details" "label": "Loan Security Details",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "loan", "fieldname": "loan",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Loan", "label": "Loan",
"options": "Loan" "options": "Loan",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "loan_application", "fieldname": "loan_application",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Loan Application", "label": "Loan Application",
"options": "Loan Application" "options": "Loan Application",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "total_security_value", "fieldname": "total_security_value",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Security Value", "label": "Total Security Value",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "maximum_loan_value", "fieldname": "maximum_loan_value",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Maximum Loan Value", "label": "Maximum Loan Value",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "loan_details_section", "fieldname": "loan_details_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Loan Details" "label": "Loan Details",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "Requested", "default": "Requested",
@@ -94,37 +112,49 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"options": "Requested\nUnpledged\nPledged\nPartially Pledged", "options": "Requested\nUnpledged\nPledged\nPartially Pledged\nCancelled",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "pledge_time", "fieldname": "pledge_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Pledge Time", "label": "Pledge Time",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "securities", "fieldname": "securities",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Securities", "label": "Securities",
"options": "Pledge", "options": "Pledge",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_10", "fieldname": "section_break_10",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Totals" "label": "Totals",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fetch_from": "loan.applicant_type", "fetch_from": "loan.applicant_type",
@@ -132,35 +162,45 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Applicant Type", "label": "Applicant Type",
"options": "Employee\nMember\nCustomer", "options": "Employee\nMember\nCustomer",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "more_information_section", "fieldname": "more_information_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "More Information" "label": "More Information",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "reference_no", "fieldname": "reference_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Reference No" "label": "Reference No",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_18", "fieldname": "column_break_18",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Description" "label": "Description",
"show_days": 1,
"show_seconds": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 18:23:16.953305", "modified": "2021-06-29 17:15:16.082256",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Security Pledge", "name": "Loan Security Pledge",

View File

@@ -23,6 +23,12 @@ class LoanSecurityPledge(Document):
update_shortfall_status(self.loan, self.total_security_value) update_shortfall_status(self.loan, self.total_security_value)
update_loan(self.loan, self.maximum_loan_value) update_loan(self.loan, self.maximum_loan_value)
def on_cancel(self):
if self.loan:
self.db_set("status", "Cancelled")
self.db_set("pledge_time", None)
update_loan(self.loan, self.maximum_loan_value, cancel=1)
def validate_duplicate_securities(self): def validate_duplicate_securities(self):
security_list = [] security_list = []
for security in self.securities: for security in self.securities:
@@ -36,7 +42,7 @@ class LoanSecurityPledge(Document):
existing_pledge = '' existing_pledge = ''
if self.loan: if self.loan:
existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan}, ['name']) existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan, 'docstatus': 1}, ['name'])
if existing_pledge: if existing_pledge:
loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type']) loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type'])
@@ -77,8 +83,12 @@ class LoanSecurityPledge(Document):
self.total_security_value = total_security_value self.total_security_value = total_security_value
self.maximum_loan_value = maximum_loan_value self.maximum_loan_value = maximum_loan_value
def update_loan(loan, maximum_value_against_pledge): def update_loan(loan, maximum_value_against_pledge, cancel=0):
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount']) maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 if cancel:
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s
WHERE name=%s""", (maximum_loan_value - maximum_value_against_pledge, loan))
else:
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))

View File

@@ -13,7 +13,7 @@ frappe.ui.form.on('Blanket Order', {
refresh: function(frm) { refresh: function(frm) {
erpnext.hide_company(); erpnext.hide_company();
if (frm.doc.customer && frm.doc.docstatus === 1) { if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
frm.add_custom_button(__("Sales Order"), function() { frm.add_custom_button(__("Sales Order"), function() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order", method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2018-05-24 07:18:08.256060", "creation": "2018-05-24 07:18:08.256060",
"doctype": "DocType", "doctype": "DocType",
@@ -79,6 +80,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"allow_on_submit": 1,
"fieldname": "to_date", "fieldname": "to_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "To Date", "label": "To Date",
@@ -129,8 +131,10 @@
"label": "Terms and Conditions Details" "label": "Terms and Conditions Details"
} }
], ],
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-11-18 19:37:37.151686", "links": [],
"modified": "2021-06-29 00:30:30.621636",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Blanket Order", "name": "Blanket Order",

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

@@ -36,6 +36,9 @@
"materials_section", "materials_section",
"inspection_required", "inspection_required",
"quality_inspection_template", "quality_inspection_template",
"column_break_31",
"bom_level",
"section_break_33",
"items", "items",
"scrap_section", "scrap_section",
"scrap_items", "scrap_items",
@@ -513,6 +516,22 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_31",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "bom_level",
"fieldtype": "Int",
"label": "BOM Level",
"read_only": 1
},
{
"fieldname": "section_break_33",
"fieldtype": "Section Break",
"hide_border": 1
} }
], ],
"icon": "fa fa-sitemap", "icon": "fa fa-sitemap",
@@ -520,7 +539,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-16 12:25:09.081968", "modified": "2021-05-16 12:25:09.081968",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@@ -154,6 +154,7 @@ class BOM(WebsiteGenerator):
self.calculate_cost() self.calculate_cost()
self.update_stock_qty() self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
self.set_bom_level()
def get_context(self, context): def get_context(self, context):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }] context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
@@ -329,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:
@@ -676,6 +677,19 @@ class BOM(WebsiteGenerator):
"""Get a complete tree representation preserving order of child items.""" """Get a complete tree representation preserving order of child items."""
return BOMTree(self.name) return BOMTree(self.name)
def set_bom_level(self, update=False):
levels = []
self.bom_level = 0
for row in self.items:
if row.bom_no:
levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0)
if levels:
self.bom_level = max(levels) + 1
if update:
self.db_set("bom_level", self.bom_level)
def get_bom_item_rate(args, bom_doc): def get_bom_item_rate(args, bom_doc):
if bom_doc.rm_cost_as_per == 'Valuation Rate': if bom_doc.rm_cost_as_per == 'Valuation Rate':
@@ -699,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()
@@ -733,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
@@ -860,7 +875,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
frappe.form_dict.parent = parent frappe.form_dict.parent = parent
if frappe.form_dict.parent: if frappe.form_dict.parent:
bom_doc = frappe.get_doc("BOM", frappe.form_dict.parent) bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent)
frappe.has_permission("BOM", doc=bom_doc, throw=True) frappe.has_permission("BOM", doc=bom_doc, throw=True)
bom_items = frappe.get_all('BOM Item', bom_items = frappe.get_all('BOM Item',
@@ -871,7 +886,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
item_names = tuple(d.get('item_code') for d in bom_items) item_names = tuple(d.get('item_code') for d in bom_items)
items = frappe.get_list('Item', items = frappe.get_list('Item',
fields=['image', 'description', 'name', 'stock_uom', 'item_name'], fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'],
filters=[['name', 'in', item_names]]) # to get only required item dicts filters=[['name', 'in', item_names]]) # to get only required item dicts
for bom_item in bom_items: for bom_item in bom_items:
@@ -884,6 +899,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
bom_item.parent_bom_qty = bom_doc.quantity bom_item.parent_bom_qty = bom_doc.quantity
bom_item.expandable = 0 if bom_item.value in ('', None) else 1 bom_item.expandable = 0 if bom_item.value in ('', None) else 1
bom_item.image = frappe.db.escape(bom_item.image)
return bom_items return bom_items
@@ -1053,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:
@@ -1068,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)
@@ -1100,6 +1109,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
}, },
'BOM Item': { 'BOM Item': {
'doctype': 'BOM Item', 'doctype': 'BOM Item',
# stop get_mapped_doc copying parent bom_no to children
'field_no_map': ['bom_no'],
'condition': lambda doc: doc.has_variants == 0 'condition': lambda doc: doc.has_variants == 0
}, },
}, target_doc, postprocess) }, target_doc, postprocess)

View File

@@ -1,13 +1,31 @@
<div style="padding: 15px;"> <div style="padding: 15px;">
{% if data.image %} <div class="row mb-5">
<img class="responsive" src={{ data.image }}> <div class="col-md-5" style="max-height: 500px">
<hr style="margin: 15px -15px;"> {% if data.image %}
{% endif %} <div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
<h4> <img class="responsive" src={{ data.image }}>
{{ __("Description") }} </div>
</h4> {% endif %}
<div style="padding-top: 10px;"> </div>
{{ data.description }} <div class="col-md-7 h-500">
<h4>
{{ __("Description") }}
</h4>
<div style="padding-top: 10px;">
{{ data.description }}
</div>
<hr style="margin: 15px -15px;">
<p>
{% if data.value %}
<a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/BOM/{{ data.value }}">
{{ __("Open BOM {0}", [data.value.bold()]) }}</a>
{% endif %}
{% if data.item_code %}
<a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
{{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
{% endif %}
</p>
</div>
</div> </div>
<hr style="margin: 15px -15px;"> <hr style="margin: 15px -15px;">
<p> <p>

View File

@@ -64,7 +64,7 @@ frappe.treeview_settings["BOM"] = {
if(node.is_root && node.data.value!="BOM") { if(node.is_root && node.data.value!="BOM") {
frappe.model.with_doc("BOM", node.data.value, function() { frappe.model.with_doc("BOM", node.data.value, function() {
var bom = frappe.model.get_doc("BOM", node.data.value); var bom = frappe.model.get_doc("BOM", node.data.value);
node.data.image = bom.image || ""; node.data.image = escape(bom.image) || "";
node.data.description = bom.description || ""; node.data.description = bom.description || "";
}); });
} }

View File

@@ -192,15 +192,20 @@ 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"):
for name in employees: new_args = frappe._dict({
self.append("time_logs", { "from_time": get_datetime(args.get("start_time")),
"from_time": get_datetime(args.get("start_time")), "operation": args.get("sub_operation"),
"employee": name.get('employee'), "completed_qty": 0.0
"operation": args.get("sub_operation"), })
"completed_qty": 0.0
})
if not self.employee: if employees:
for name in employees:
new_args.employee = name.get('employee')
self.add_start_time_log(new_args)
else:
self.add_start_time_log(new_args)
if not self.employee and employees:
self.set_employees(employees) self.set_employees(employees)
if self.status == "On Hold": if self.status == "On Hold":
@@ -208,6 +213,9 @@ class JobCard(Document):
self.save() self.save()
def add_start_time_log(self, args):
self.append("time_logs", args)
def set_employees(self, employees): def set_employees(self, employees):
for name in employees: for name in employees:
self.append('employee', { self.append('employee', {

View File

@@ -4,7 +4,7 @@
frappe.ui.form.on('Production Plan', { frappe.ui.form.on('Production Plan', {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Work Order': 'Work Order', 'Work Order': 'Work Order / Subcontract PO',
'Material Request': 'Material Request', 'Material Request': 'Material Request',
}; };
@@ -68,17 +68,13 @@ frappe.ui.form.on('Production Plan', {
frm.trigger("show_progress"); frm.trigger("show_progress");
if (frm.doc.status !== "Completed") { if (frm.doc.status !== "Completed") {
if (frm.doc.po_items && frm.doc.status !== "Closed") { frm.add_custom_button(__("Work Order Tree"), ()=> {
frm.add_custom_button(__("Work Order"), ()=> { frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name});
frm.trigger("make_work_order"); }, __('View'));
}, __('Create'));
}
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { frm.add_custom_button(__("Production Plan Summary"), ()=> {
frm.add_custom_button(__("Material Request"), ()=> { frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name});
frm.trigger("make_material_request"); }, __('View'));
}, __('Create'));
}
if (frm.doc.status === "Closed") { if (frm.doc.status === "Closed") {
frm.add_custom_button(__("Re-open"), function() { frm.add_custom_button(__("Re-open"), function() {
@@ -89,6 +85,18 @@ frappe.ui.form.on('Production Plan', {
frm.events.close_open_production_plan(frm, true); frm.events.close_open_production_plan(frm, true);
}, __("Status")); }, __("Status"));
} }
if (frm.doc.po_items && frm.doc.status !== "Closed") {
frm.add_custom_button(__("Work Order / Subcontract PO"), ()=> {
frm.trigger("make_work_order");
}, __('Create'));
}
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
frm.add_custom_button(__("Material Request"), ()=> {
frm.trigger("make_material_request");
}, __('Create'));
}
} }
} }
@@ -233,6 +241,17 @@ frappe.ui.form.on('Production Plan', {
}); });
}, },
get_sub_assembly_items: function(frm) {
frappe.call({
method: "get_sub_assembly_items",
freeze: true,
doc: frm.doc,
callback: function() {
refresh_field("sub_assembly_items");
}
});
},
get_items_for_mr: function(frm) { get_items_for_mr: function(frm) {
if (!frm.doc.for_warehouse) { if (!frm.doc.for_warehouse) {
frappe.throw(__("Select warehouse for material requests")); frappe.throw(__("Select warehouse for material requests"));

View File

@@ -32,6 +32,9 @@
"po_items", "po_items",
"section_break_25", "section_break_25",
"prod_plan_references", "prod_plan_references",
"section_break_24",
"get_sub_assembly_items",
"sub_assembly_items",
"material_request_planning", "material_request_planning",
"include_non_stock_items", "include_non_stock_items",
"include_subcontracted_items", "include_subcontracted_items",
@@ -187,7 +190,7 @@
"depends_on": "get_items_from", "depends_on": "get_items_from",
"fieldname": "get_items", "fieldname": "get_items",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Get Items For Work Order" "label": "Get Finished Goods for Manufacture"
}, },
{ {
"fieldname": "po_items", "fieldname": "po_items",
@@ -199,7 +202,7 @@
{ {
"fieldname": "material_request_planning", "fieldname": "material_request_planning",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Material Request Planning" "label": "Material Requirement Planning"
}, },
{ {
"default": "1", "default": "1",
@@ -237,12 +240,13 @@
}, },
{ {
"fieldname": "section_break_27", "fieldname": "section_break_27",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"hide_border": 1
}, },
{ {
"fieldname": "mr_items", "fieldname": "mr_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Material Request Plan Item", "label": "Raw Materials",
"no_copy": 1, "no_copy": 1,
"options": "Material Request Plan Item" "options": "Material Request Plan Item"
}, },
@@ -337,13 +341,30 @@
"hidden": 1, "hidden": 1,
"label": "Production Plan Item Reference", "label": "Production Plan Item Reference",
"options": "Production Plan Item Reference" "options": "Production Plan Item Reference"
},
{
"fieldname": "section_break_24",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "sub_assembly_items",
"fieldtype": "Table",
"label": "Sub Assembly Items",
"no_copy": 1,
"options": "Production Plan Sub Assembly Item"
},
{
"fieldname": "get_sub_assembly_items",
"fieldtype": "Button",
"label": "Get Sub Assembly Items"
} }
], ],
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-05-24 16:59:03.643211", "modified": "2021-06-28 20:00:33.905114",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Plan", "name": "Production Plan",

View File

@@ -5,10 +5,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, json, copy import frappe, json, copy
from frappe import msgprint, _ from frappe import msgprint, _
from six import string_types, iteritems from six import iteritems
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil from frappe.utils import (flt, cint, nowdate, add_days, comma_and, now_datetime,
ceil, get_link_to_form, getdate)
from frappe.utils.csvutils import build_csv_response from frappe.utils.csvutils import build_csv_response
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
@@ -349,49 +350,88 @@ class ProductionPlan(Document):
@frappe.whitelist() @frappe.whitelist()
def make_work_order(self): def make_work_order(self):
wo_list = [] wo_list, po_list = [], []
subcontracted_po = {}
self.validate_data() self.validate_data()
self.make_work_order_for_finished_goods(wo_list)
self.make_work_order_for_subassembly_items(wo_list, subcontracted_po)
self.make_subcontracted_purchase_order(subcontracted_po, po_list)
self.show_list_created_message('Work Order', wo_list)
self.show_list_created_message('Purchase Order', po_list)
def make_work_order_for_finished_goods(self, wo_list):
items_data = self.get_production_items() items_data = self.get_production_items()
for key, item in items_data.items(): for key, item in items_data.items():
if self.sub_assembly_items:
item['use_multi_level_bom'] = 0
work_order = self.create_work_order(item) work_order = self.create_work_order(item)
if work_order: if work_order:
wo_list.append(work_order) wo_list.append(work_order)
if item.get("make_work_order_for_sub_assembly_items"): def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po):
work_orders = self.make_work_order_for_sub_assembly_items(item) for row in self.sub_assembly_items:
wo_list.extend(work_orders) if row.type_of_manufacturing == 'Subcontract':
subcontracted_po.setdefault(row.supplier, []).append(row)
continue
args = {}
self.prepare_args_for_sub_assembly_items(row, args)
work_order = self.create_work_order(args)
if work_order:
wo_list.append(work_order)
def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders):
if not subcontracted_po:
return
for supplier, po_list in subcontracted_po.items():
po = frappe.new_doc('Purchase Order')
po.supplier = supplier
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
po.is_subcontracted_item = 'Yes'
for row in po_list:
args = {
'item_code': row.production_item,
'warehouse': row.fg_warehouse,
'production_plan_sub_assembly_item': row.name,
'bom': row.bom_no,
'production_plan': self.name
}
for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name',
'description', 'production_plan_item']:
args[field] = row.get(field)
po.append('items', args)
po.set_missing_values()
po.flags.ignore_mandatory = True
po.flags.ignore_validate = True
po.insert()
purchase_orders.append(po.name)
def show_list_created_message(self, doctype, doc_list=None):
if not doc_list:
return
frappe.flags.mute_messages = False frappe.flags.mute_messages = False
if doc_list:
doc_list = [get_link_to_form(doctype, p) for p in doc_list]
msgprint(_("{0} created").format(comma_and(doc_list)))
if wo_list: def prepare_args_for_sub_assembly_items(self, row, args):
wo_list = ["""<a href="/app/Form/Work Order/%s" target="_blank">%s</a>""" % \ for field in ["production_item", "item_name", "qty", "fg_warehouse",
(p, p) for p in wo_list] "description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]:
msgprint(_("{0} created").format(comma_and(wo_list))) args[field] = row.get(field)
else :
msgprint(_("No Work Orders created"))
def make_work_order_for_sub_assembly_items(self, item): args.update({
work_orders = [] "use_multi_level_bom": 0,
bom_data = {} "production_plan": self.name,
"production_plan_sub_assembly_item": row.name
get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty")) })
for key, data in bom_data.items():
data.update({
'qty': data.get("stock_qty"),
'production_plan': self.name,
'use_multi_level_bom': item.get("use_multi_level_bom"),
'company': self.company,
'fg_warehouse': item.get("fg_warehouse"),
'update_consumed_material_cost_in_project': 0
})
work_order = self.create_work_order(data)
if work_order:
work_orders.append(work_order)
return work_orders
def create_work_order(self, item): def create_work_order(self, item):
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse
@@ -476,9 +516,32 @@ class ProductionPlan(Document):
else : else :
msgprint(_("No material request created")) msgprint(_("No material request created"))
@frappe.whitelist()
def get_sub_assembly_items(self, manufacturing_type=None):
self.sub_assembly_items = []
for row in self.po_items:
bom_data = []
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
self.save()
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
data.fg_warehouse = row.warehouse
data.schedule_date = row.planned_start_date
data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
else "In House")
self.append("sub_assembly_items", data)
@frappe.whitelist() @frappe.whitelist()
def download_raw_materials(doc, warehouses=None): def download_raw_materials(doc, warehouses=None):
if isinstance(doc, string_types): if isinstance(doc, str):
doc = frappe._dict(json.loads(doc)) doc = frappe._dict(json.loads(doc))
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
@@ -660,7 +723,7 @@ def get_sales_orders(self):
@frappe.whitelist() @frappe.whitelist()
def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
if isinstance(row, string_types): if isinstance(row, str):
row = frappe._dict(json.loads(row)) row = frappe._dict(json.loads(row))
company = frappe.db.escape(company) company = frappe.db.escape(company)
@@ -684,8 +747,10 @@ 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=[]): def get_warehouse_list(warehouses):
if isinstance(warehouses, string_types): warehouse_list = []
if isinstance(warehouses, str):
warehouses = json.loads(warehouses) warehouses = json.loads(warehouses)
for row in warehouses: for row in warehouses:
@@ -695,23 +760,19 @@ def get_warehouse_list(warehouses, warehouse_list=[]):
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, string_types): 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')
@@ -726,6 +787,9 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
so_item_details = frappe._dict() so_item_details = frappe._dict()
for data in po_items: for data in po_items:
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
data["include_exploded_items"] = 1
planned_qty = data.get('required_qty') or data.get('planned_qty') planned_qty = data.get('required_qty') or data.get('planned_qty')
ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty
warehouse = doc.get('for_warehouse') warehouse = doc.get('for_warehouse')
@@ -857,23 +921,28 @@ def get_item_data(item_code):
# "description": item_details.get("description") # "description": item_details.get("description")
} }
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty): def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
data = get_children('BOM', parent = bom_no) data = get_children('BOM', parent = bom_no)
for d in data: for d in data:
if d.expandable: if d.expandable:
key = (d.name, d.value) parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
if key not in bom_data: bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level")
bom_data.setdefault(key, { if d.value else 0)
'stock_qty': 0,
'description': d.description,
'production_item': d.item_code,
'item_name': d.item_name,
'stock_uom': d.stock_uom,
'uom': d.stock_uom,
'bom_no': d.value
})
bom_item = bom_data.get(key) stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) bom_data.append(frappe._dict({
'parent_item_code': parent_item_code,
'description': d.description,
'production_item': d.item_code,
'item_name': d.item_name,
'stock_uom': d.stock_uom,
'uom': d.stock_uom,
'bom_no': d.value,
'is_sub_contracted_item': d.is_sub_contracted_item,
'bom_level': bom_level,
'indent': indent,
'stock_qty': stock_qty
}))
get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"]) if d.value:
get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1)

View File

@@ -9,5 +9,9 @@ def get_data():
'label': _('Transactions'), 'label': _('Transactions'),
'items': ['Work Order', 'Material Request'] 'items': ['Work Order', 'Material Request']
}, },
{
'label': _('Subcontract'),
'items': ['Purchase Order']
},
] ]
} }

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):
@@ -169,7 +169,7 @@ class TestProductionPlan(unittest.TestCase):
pln.get_items() pln.get_items()
pln.submit() pln.submit()
self.assertTrue(pln.po_items[0].planned_qty, 3) self.assertTrue(pln.po_items[0].planned_qty, 3)
pln.make_work_order() pln.make_work_order()
work_order = frappe.db.get_value('Work Order', { work_order = frappe.db.get_value('Work Order', {
@@ -193,10 +193,10 @@ class TestProductionPlan(unittest.TestCase):
for so_item in so_items: for so_item in so_items:
so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty') so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty')
self.assertEqual(so_wo_qty, 0.0) self.assertEqual(so_wo_qty, 0.0)
latest_plan = frappe.get_doc('Production Plan', pln.name) latest_plan = frappe.get_doc('Production Plan', pln.name)
latest_plan.cancel() latest_plan.cancel()
def test_pp_to_mr_customer_provided(self): def test_pp_to_mr_customer_provided(self):
#Material Request from Production Plan for Customer Provided #Material Request from Production Plan for Customer Provided
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
@@ -236,10 +236,10 @@ class TestProductionPlan(unittest.TestCase):
pln.append("po_items", { pln.append("po_items", {
"item_code": item_code, "item_code": item_code,
"bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}), "bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
"planned_qty": 3, "planned_qty": 3
"make_work_order_for_sub_assembly_items": 1
}) })
pln.get_sub_assembly_items('In House')
pln.submit() pln.submit()
pln.make_work_order() pln.make_work_order()
@@ -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

@@ -9,18 +9,17 @@
"include_exploded_items", "include_exploded_items",
"item_code", "item_code",
"bom_no", "bom_no",
"planned_qty",
"column_break_6", "column_break_6",
"make_work_order_for_sub_assembly_items", "planned_qty",
"warehouse", "warehouse",
"planned_start_date", "planned_start_date",
"section_break_9", "section_break_9",
"pending_qty", "pending_qty",
"ordered_qty", "ordered_qty",
"produced_qty",
"column_break_17", "column_break_17",
"description", "description",
"stock_uom", "stock_uom",
"produced_qty",
"reference_section", "reference_section",
"sales_order", "sales_order",
"sales_order_item", "sales_order_item",
@@ -32,11 +31,10 @@
], ],
"fields": [ "fields": [
{ {
"columns": 2, "columns": 1,
"default": "0", "default": "1",
"fieldname": "include_exploded_items", "fieldname": "include_exploded_items",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1,
"label": "Include Exploded Items" "label": "Include Exploded Items"
}, },
{ {
@@ -80,13 +78,6 @@
"fieldname": "column_break_6", "fieldname": "column_break_6",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"default": "0",
"description": "If enabled, system will create the work order for the exploded items against which BOM is available.",
"fieldname": "make_work_order_for_sub_assembly_items",
"fieldtype": "Check",
"label": "Make Work Order for Sub Assembly Items"
},
{ {
"fieldname": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "fieldtype": "Link",
@@ -218,7 +209,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-04-28 19:14:57.772123", "modified": "2021-06-28 18:31:06.822168",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Plan Item", "name": "Production Plan Item",

View File

@@ -0,0 +1,202 @@
{
"actions": [],
"creation": "2020-12-27 16:08:36.127199",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"production_item",
"item_name",
"fg_warehouse",
"parent_item_code",
"schedule_date",
"column_break_3",
"qty",
"bom_no",
"bom_level",
"type_of_manufacturing",
"supplier",
"work_order_details_section",
"work_order",
"purchase_order",
"production_plan_item",
"column_break_7",
"produced_qty",
"received_qty",
"indent",
"section_break_19",
"uom",
"stock_uom",
"column_break_22",
"description"
],
"fields": [
{
"fetch_from": "sub_assembly_item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.type_of_manufacturing == \"In House\"",
"fieldname": "work_order_details_section",
"fieldtype": "Section Break",
"label": "Reference"
},
{
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
"options": "Work Order",
"read_only": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"columns": 1,
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Required Qty",
"read_only": 1
},
{
"fieldname": "purchase_order",
"fieldtype": "Link",
"label": "Purchase Order",
"options": "Purchase Order",
"read_only": 1
},
{
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty"
},
{
"fieldname": "bom_no",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Bom No",
"options": "BOM"
},
{
"fieldname": "production_plan_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Production Plan Item",
"read_only": 1
},
{
"fieldname": "parent_item_code",
"fieldtype": "Link",
"label": "Finished Good",
"options": "Item",
"read_only": 1
},
{
"columns": 1,
"fetch_from": "bom_no.bom_level",
"fieldname": "bom_level",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Level (BOM)",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_19",
"fieldtype": "Section Break",
"label": "Item Details"
},
{
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM",
"read_only": 1
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
},
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "description",
"read_only": 1
},
{
"fieldname": "production_item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sub Assembly Item Code",
"options": "Item",
"read_only": 1
},
{
"fieldname": "indent",
"fieldtype": "Int",
"label": "Indent"
},
{
"fieldname": "fg_warehouse",
"fieldtype": "Link",
"label": "Target Warehouse",
"options": "Warehouse"
},
{
"fieldname": "produced_qty",
"fieldtype": "Data",
"label": "Produced Quantity",
"read_only": 1
},
{
"default": "In House",
"fieldname": "type_of_manufacturing",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Manufacturing Type",
"options": "In House\nSubcontract"
},
{
"fieldname": "supplier",
"fieldtype": "Link",
"label": "Supplier",
"mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract'",
"options": "Supplier"
},
{
"fieldname": "schedule_date",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Schedule Date"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-28 20:10:56.296410",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class ProductionPlanSubAssemblyItem(Document):
pass

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",

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