mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 20:05:09 +00:00
Merge branch 'version-13-hotfix' into feat-bom-process-loss
This commit is contained in:
@@ -98,8 +98,6 @@ rules:
|
||||
languages: [python]
|
||||
severity: WARNING
|
||||
paths:
|
||||
exclude:
|
||||
- test_*.py
|
||||
include:
|
||||
- "*/**/doctype/*"
|
||||
|
||||
|
||||
36
.github/workflows/semgrep.yml
vendored
36
.github/workflows/semgrep.yml
vendored
@@ -1,34 +1,18 @@
|
||||
name: Semgrep
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-13-hotfix
|
||||
- version-13-pre-release
|
||||
pull_request: { }
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup semgrep
|
||||
run: |
|
||||
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
|
||||
- uses: actions/checkout@v2
|
||||
- uses: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
.github/helper/semgrep_rules
|
||||
|
||||
43
CODEOWNERS
43
CODEOWNERS
@@ -3,16 +3,33 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
manufacturing/ @rohitwaghchaure @marination
|
||||
accounts/ @deepeshgarg007 @nextchamp-saqib
|
||||
loan_management/ @deepeshgarg007 @rohitwaghchaure
|
||||
pos* @nextchamp-saqib @rohitwaghchaure
|
||||
assets/ @nextchamp-saqib @deepeshgarg007
|
||||
stock/ @marination @rohitwaghchaure
|
||||
buying/ @marination @deepeshgarg007
|
||||
hr/ @Anurag810 @rohitwaghchaure
|
||||
projects/ @hrwX @nextchamp-saqib
|
||||
support/ @hrwX @marination
|
||||
healthcare/ @ruchamahabal @marination
|
||||
erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib
|
||||
requirements.txt @gavindsouza
|
||||
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/assets/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/erpnext_integrations/ @nextchamp-saqib
|
||||
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/regional @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/selling @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
|
||||
erpnext/buying/ @marination @rohitwaghchaure @ankush
|
||||
erpnext/e_commerce/ @marination
|
||||
erpnext/maintenance/ @marination @rohitwaghchaure
|
||||
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
|
||||
|
||||
@@ -301,17 +301,21 @@ def process_deferred_accounting(posting_date=None):
|
||||
start_date = add_months(today(), -1)
|
||||
end_date = add_days(today(), -1)
|
||||
|
||||
for record_type in ('Income', 'Expense'):
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type
|
||||
))
|
||||
companies = frappe.get_all('Company')
|
||||
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
for company in companies:
|
||||
for record_type in ('Income', 'Expense'):
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type
|
||||
))
|
||||
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
|
||||
def make_gl_entries(doc, credit_account, debit_account, against,
|
||||
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
|
||||
|
||||
@@ -51,7 +51,7 @@ class BankStatementImport(DataImport):
|
||||
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"))
|
||||
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
|
||||
@@ -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
|
||||
|
||||
class ChartofAccountsImporter(Document):
|
||||
pass
|
||||
def validate(self):
|
||||
validate_accounts(self.import_file)
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_company(company):
|
||||
@@ -301,28 +302,27 @@ def validate_accounts(file_name):
|
||||
if account["parent_account"] and accounts_dict.get(account["parent_account"]):
|
||||
accounts_dict[account["parent_account"]]["is_group"] = 1
|
||||
|
||||
message = validate_root(accounts_dict)
|
||||
if message: return message
|
||||
message = validate_account_types(accounts_dict)
|
||||
if message: return message
|
||||
validate_root(accounts_dict)
|
||||
|
||||
validate_account_types(accounts_dict)
|
||||
|
||||
return [True, len(accounts)]
|
||||
|
||||
def validate_root(accounts):
|
||||
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
|
||||
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 = []
|
||||
|
||||
for account in roots:
|
||||
if not account.get("root_type") and account.get("account_name"):
|
||||
error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name")))
|
||||
error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name")))
|
||||
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
|
||||
error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name")))
|
||||
error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
|
||||
|
||||
if error_messages:
|
||||
return "<br>".join(error_messages)
|
||||
frappe.throw("<br>".join(error_messages))
|
||||
|
||||
def get_root_types():
|
||||
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))
|
||||
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"]
|
||||
# fix logic bug
|
||||
@@ -364,7 +364,7 @@ def validate_account_types(accounts):
|
||||
|
||||
missing = list(set(account_types_for_group) - set(account_groups))
|
||||
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):
|
||||
linked = frappe.db.sql('''select fieldname from tabDocField
|
||||
@@ -391,5 +391,5 @@ def set_default_accounts(company):
|
||||
})
|
||||
|
||||
company.save()
|
||||
install_country_fixtures(company.name)
|
||||
install_country_fixtures(company.name, company.country)
|
||||
company.create_default_tax_template()
|
||||
|
||||
@@ -25,7 +25,7 @@ class Dunning(AccountsController):
|
||||
|
||||
def validate_amount(self):
|
||||
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'):
|
||||
self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount'))
|
||||
if self.dunning_amount != amounts.get('dunning_amount'):
|
||||
@@ -91,13 +91,13 @@ def resolve_dunning(doc, state):
|
||||
for dunning in dunnings:
|
||||
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
|
||||
grand_total = 0
|
||||
grand_total = flt(outstanding_amount) + flt(dunning_fee)
|
||||
if rate_of_interest:
|
||||
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
|
||||
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)
|
||||
return {
|
||||
'interest_amount': interest_amount,
|
||||
|
||||
@@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
create_dunning_type()
|
||||
create_dunning_type_with_zero_interest_rate()
|
||||
unlink_payment_on_cancel_of_invoice()
|
||||
|
||||
@classmethod
|
||||
@@ -25,11 +26,20 @@ class TestDunning(unittest.TestCase):
|
||||
def test_dunning(self):
|
||||
dunning = create_dunning()
|
||||
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('dunning_amount'), 2), 20.44)
|
||||
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
|
||||
|
||||
def test_dunning_with_zero_interest_rate(self):
|
||||
dunning = create_dunning_with_zero_interest_rate()
|
||||
amounts = calculate_interest_and_amount(
|
||||
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
|
||||
self.assertEqual(round(amounts.get('interest_amount'), 2), 0)
|
||||
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20)
|
||||
self.assertEqual(round(amounts.get('grand_total'), 2), 120)
|
||||
|
||||
|
||||
def test_gl_entries(self):
|
||||
dunning = create_dunning()
|
||||
dunning.submit()
|
||||
@@ -83,6 +93,27 @@ def create_dunning():
|
||||
dunning.save()
|
||||
return dunning
|
||||
|
||||
def create_dunning_with_zero_interest_rate():
|
||||
posting_date = add_days(today(), -20)
|
||||
due_date = add_days(today(), -15)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=posting_date, due_date=due_date, status='Overdue')
|
||||
dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest')
|
||||
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():
|
||||
dunning_type = frappe.new_doc("Dunning Type")
|
||||
dunning_type.dunning_type = 'First Notice'
|
||||
@@ -98,3 +129,19 @@ def create_dunning_type():
|
||||
}
|
||||
)
|
||||
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()
|
||||
@@ -667,6 +667,7 @@
|
||||
{
|
||||
"fieldname": "base_paid_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Paid Amount After Tax (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
@@ -693,21 +694,25 @@
|
||||
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
|
||||
"fieldname": "received_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Received Amount After Tax",
|
||||
"options": "paid_to_account_currency"
|
||||
"options": "paid_to_account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "doc.received_amount",
|
||||
"fieldname": "base_received_amount_after_tax",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"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,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-22 20:37:06.154206",
|
||||
"modified": "2021-07-09 08:58:15.008761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -183,6 +183,13 @@ class PaymentEntry(AccountsController):
|
||||
d.reference_name, self.party_account_currency)
|
||||
|
||||
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:
|
||||
d.db_set(field, value)
|
||||
|
||||
@@ -404,9 +411,15 @@ class PaymentEntry(AccountsController):
|
||||
if not self.advance_tax_account:
|
||||
frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
|
||||
|
||||
reference_doclist = []
|
||||
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
|
||||
args = frappe._dict({
|
||||
@@ -423,7 +436,7 @@ class PaymentEntry(AccountsController):
|
||||
return
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -512,16 +525,19 @@ class PaymentEntry(AccountsController):
|
||||
self.unallocated_amount = 0
|
||||
if self.party:
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
included_taxes = self.get_included_taxes()
|
||||
if self.payment_type == "Receive" \
|
||||
and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \
|
||||
and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate):
|
||||
self.unallocated_amount = (self.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 + (total_deductions / self.source_exchange_rate):
|
||||
self.unallocated_amount = (self.received_amount + total_deductions -
|
||||
self.base_total_allocated_amount) / self.source_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
elif self.payment_type == "Pay" \
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount_after_tax - total_deductions) \
|
||||
and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate):
|
||||
self.unallocated_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 + (total_deductions / self.target_exchange_rate):
|
||||
self.unallocated_amount = (self.base_paid_amount - (total_deductions +
|
||||
self.base_total_allocated_amount)) / self.target_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
|
||||
def set_difference_amount(self):
|
||||
base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate)
|
||||
@@ -530,17 +546,29 @@ class PaymentEntry(AccountsController):
|
||||
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
|
||||
|
||||
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":
|
||||
self.difference_amount = self.base_paid_amount_after_tax - base_party_amount
|
||||
self.difference_amount = self.base_paid_amount - base_party_amount
|
||||
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"))
|
||||
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"))
|
||||
|
||||
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.
|
||||
# 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):
|
||||
@@ -664,8 +692,8 @@ class PaymentEntry(AccountsController):
|
||||
gl_entries.append(gle)
|
||||
|
||||
if self.unallocated_amount:
|
||||
base_unallocated_amount = self.unallocated_amount * \
|
||||
(self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate)
|
||||
exchange_rate = self.get_exchange_rate()
|
||||
base_unallocated_amount = (self.unallocated_amount * exchange_rate)
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
@@ -683,8 +711,8 @@ class PaymentEntry(AccountsController):
|
||||
"account": self.paid_from,
|
||||
"account_currency": self.paid_from_account_currency,
|
||||
"against": self.party if self.payment_type=="Pay" else self.paid_to,
|
||||
"credit_in_account_currency": self.paid_amount_after_tax,
|
||||
"credit": self.base_paid_amount_after_tax,
|
||||
"credit_in_account_currency": self.paid_amount,
|
||||
"credit": self.base_paid_amount,
|
||||
"cost_center": self.cost_center
|
||||
}, item=self)
|
||||
)
|
||||
@@ -694,8 +722,8 @@ class PaymentEntry(AccountsController):
|
||||
"account": self.paid_to,
|
||||
"account_currency": self.paid_to_account_currency,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
"debit_in_account_currency": self.received_amount_after_tax,
|
||||
"debit": self.base_received_amount_after_tax,
|
||||
"debit_in_account_currency": self.received_amount,
|
||||
"debit": self.base_received_amount,
|
||||
"cost_center": self.cost_center
|
||||
}, item=self)
|
||||
)
|
||||
@@ -708,35 +736,42 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
if self.payment_type in ('Pay', 'Internal Transfer'):
|
||||
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
|
||||
against = self.party or self.paid_from
|
||||
elif self.payment_type == 'Receive':
|
||||
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()
|
||||
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(
|
||||
self.get_gl_dict({
|
||||
"account": d.account_head,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
dr_or_cr: d.base_tax_amount,
|
||||
dr_or_cr + "_in_account_currency": d.base_tax_amount
|
||||
"against": against,
|
||||
dr_or_cr: tax_amount,
|
||||
dr_or_cr + "_in_account_currency": base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": d.cost_center
|
||||
}, account_currency, item=d))
|
||||
|
||||
#Intentionally use -1 to get net values in party account
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": payment_or_advance_account,
|
||||
"against": self.party if self.payment_type=="Receive" else self.paid_from,
|
||||
dr_or_cr: -1 * d.base_tax_amount,
|
||||
dr_or_cr + "_in_account_currency": -1*d.base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party
|
||||
}, account_currency, item=d))
|
||||
if not d.included_in_paid_amount or self.advance_tax_account:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": payment_or_advance_account,
|
||||
"against": against,
|
||||
dr_or_cr: -1 * tax_amount,
|
||||
dr_or_cr + "_in_account_currency": -1 * base_tax_amount
|
||||
if account_currency==self.company_currency
|
||||
else d.tax_amount,
|
||||
"cost_center": self.cost_center,
|
||||
}, account_currency, item=d))
|
||||
|
||||
def add_deductions_gl_entries(self, gl_entries):
|
||||
for d in self.get("deductions"):
|
||||
@@ -760,9 +795,9 @@ class PaymentEntry(AccountsController):
|
||||
if self.advance_tax_account:
|
||||
return self.advance_tax_account
|
||||
elif self.payment_type == 'Receive':
|
||||
return self.paid_from
|
||||
elif self.payment_type in ('Pay', 'Internal Transfer'):
|
||||
return self.paid_to
|
||||
elif self.payment_type in ('Pay', 'Internal Transfer'):
|
||||
return self.paid_from
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
@@ -806,10 +841,17 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
if account_details:
|
||||
row.update(account_details)
|
||||
|
||||
if not row.get('amount'):
|
||||
# if no difference amount
|
||||
return
|
||||
|
||||
self.append('deductions', row)
|
||||
self.set_unallocated_amount()
|
||||
|
||||
def get_exchange_rate(self):
|
||||
return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate
|
||||
|
||||
def initialize_taxes(self):
|
||||
for tax in self.get("taxes"):
|
||||
validate_taxes_and_charges(tax)
|
||||
@@ -1318,9 +1360,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
|
||||
return frappe._dict({
|
||||
"due_date": ref_doc.get("due_date"),
|
||||
"total_amount": total_amount,
|
||||
"outstanding_amount": outstanding_amount,
|
||||
"exchange_rate": exchange_rate,
|
||||
"total_amount": flt(total_amount),
|
||||
"outstanding_amount": flt(outstanding_amount),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"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":
|
||||
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
|
||||
|
||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
|
||||
@@ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase):
|
||||
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(expected_account_balance, account_balance)
|
||||
self.assertEqual(expected_party_balance, party_balance)
|
||||
self.assertEqual(expected_party_account_balance, party_account_balance)
|
||||
self.assertEqual(flt(expected_account_balance), account_balance)
|
||||
self.assertEqual(flt(expected_party_balance), party_balance)
|
||||
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
|
||||
|
||||
def create_payment_terms_template():
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"total_amount",
|
||||
"outstanding_amount",
|
||||
"allocated_amount",
|
||||
"exchange_rate"
|
||||
"exchange_rate",
|
||||
"exchange_gain_loss"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -90,12 +91,19 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-10 11:25:47.144392",
|
||||
"modified": "2021-04-21 13:30:11.605388",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -207,10 +207,9 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
@frappe.whitelist()
|
||||
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
|
||||
billing_email = frappe.db.sql("""
|
||||
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 \
|
||||
c.is_billing_contact=1 \
|
||||
order by c.creation desc""")
|
||||
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=%s and c.is_billing_contact=1
|
||||
order by c.creation desc""", customer_name)
|
||||
|
||||
if len(billing_email) == 0 or (billing_email[0][0] is None):
|
||||
if billing_and_primary:
|
||||
|
||||
@@ -451,6 +451,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.get_asset_gl_entry(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.allocate_advance_taxes(gl_entries)
|
||||
|
||||
@@ -953,6 +953,120 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||
acc_settings.save()
|
||||
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
unlink_enabled = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings",
|
||||
"unlink_payment_on_cancel_of_invoice")
|
||||
|
||||
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", -40000.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", -38000.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):
|
||||
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
|
||||
@@ -1010,21 +1124,21 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
# Check GLE for Purchase Invoice
|
||||
# Zero net effect on final TDS Payable on invoice
|
||||
expected_gle = [
|
||||
['_Test Account Cost for Goods Sold - _TC', 30000, 0],
|
||||
['_Test Account Excise Duty - _TC', 0, 3000],
|
||||
['Creditors - _TC', 0, 27000],
|
||||
['TDS Payable - _TC', 3000, 3000]
|
||||
['_Test Account Cost for Goods Sold - _TC', 30000],
|
||||
['_Test Account Excise Duty - _TC', -3000],
|
||||
['Creditors - _TC', -27000],
|
||||
['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`
|
||||
where voucher_type='Purchase Invoice' and voucher_no=%s
|
||||
group by account
|
||||
order by account asc""", (purchase_invoice.name), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.debit)
|
||||
self.assertEqual(expected_gle[i][2], gle.credit)
|
||||
self.assertEqual(expected_gle[i][1], gle.amount)
|
||||
|
||||
def update_tax_witholding_category(company, account, date):
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
@@ -1,235 +1,127 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2013-03-08 15:36:46",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2013-03-08 15:36:46",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"remarks",
|
||||
"reference_row",
|
||||
"col_break1",
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"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,
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "journal_voucher",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"print_width": "180px",
|
||||
"read_only": 1,
|
||||
"width": "180px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"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
|
||||
},
|
||||
"columns": 3,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"no_copy": 1,
|
||||
"options": "reference_type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"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,
|
||||
"columns": 3,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Remarks",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "remarks",
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"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,
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Row",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "jv_detail_no",
|
||||
"oldfieldtype": "Date",
|
||||
"print_hide": 1,
|
||||
"print_width": "80px",
|
||||
"read_only": 1,
|
||||
"width": "80px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "col_break1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "advance_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 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,
|
||||
"columns": 2,
|
||||
"fieldname": "advance_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Advance Amount",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "advance_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"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,
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated Amount",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "allocated_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"print_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,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"menu_index": 0,
|
||||
"modified": "2016-08-26 02:30:54.407138",
|
||||
"modified_by": "Administrator",
|
||||
"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
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-20 16:26:53.820530",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Advance",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
@@ -840,6 +840,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_customer_gl_entry(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.allocate_advance_taxes(gl_entries)
|
||||
|
||||
@@ -1957,6 +1957,33 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
einvoice = make_einvoice(si)
|
||||
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():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
si.naming_series = 'INV-2020-.#####'
|
||||
@@ -1985,32 +2012,6 @@ def get_sales_invoice_for_e_invoice():
|
||||
|
||||
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():
|
||||
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:
|
||||
gst_settings.append("gst_accounts", {
|
||||
"company": "_Test Company",
|
||||
"cgst_account": "CGST - _TC",
|
||||
"sgst_account": "SGST - _TC",
|
||||
"igst_account": "IGST - _TC",
|
||||
"cgst_account": "Output Tax CGST - _TC",
|
||||
"sgst_account": "Output Tax SGST - _TC",
|
||||
"igst_account": "Output Tax IGST - _TC",
|
||||
})
|
||||
|
||||
gst_settings.save()
|
||||
@@ -2106,7 +2107,7 @@ def make_sales_invoice_for_ewaybill():
|
||||
|
||||
si.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "CGST - _TC",
|
||||
"account_head": "Output Tax CGST - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "CGST @ 9.0",
|
||||
"rate": 9
|
||||
@@ -2114,7 +2115,7 @@ def make_sales_invoice_for_ewaybill():
|
||||
|
||||
si.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "SGST - _TC",
|
||||
"account_head": "Output Tax SGST - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "SGST @ 9.0",
|
||||
"rate": 9
|
||||
|
||||
@@ -1,235 +1,128 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2013-02-22 01:27:41",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2013-02-22 01:27:41",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"remarks",
|
||||
"reference_row",
|
||||
"col_break1",
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"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,
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "journal_voucher",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "DocType",
|
||||
"print_width": "250px",
|
||||
"read_only": 1,
|
||||
"width": "250px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"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": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 3,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"no_copy": 1,
|
||||
"options": "reference_type",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"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,
|
||||
"columns": 3,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Remarks",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "remarks",
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"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,
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Row",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "jv_detail_no",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
"print_width": "120px",
|
||||
"read_only": 1,
|
||||
"width": "120px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "col_break1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "advance_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 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,
|
||||
"columns": 2,
|
||||
"fieldname": "advance_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Advance amount",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "advance_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"print_width": "120px",
|
||||
"read_only": 1,
|
||||
"width": "120px"
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"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,
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated amount",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "allocated_amount",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "party_account_currency",
|
||||
"print_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,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"menu_index": 0,
|
||||
"modified": "2016-08-26 02:36:10.718057",
|
||||
"modified_by": "Administrator",
|
||||
"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
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-04 20:25:49.832052",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Advance",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
@@ -1,24 +1,6 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// 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) {
|
||||
if(frm.doc.customer) {
|
||||
frappe.call({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,7 @@ class TestTaxRule(unittest.TestCase):
|
||||
tax_rule1 = make_tax_rule(customer_group= "All Customer Groups",
|
||||
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
|
||||
tax_rule1.save()
|
||||
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":0}),
|
||||
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}),
|
||||
"_Test Sales Taxes and Charges Template - _TC")
|
||||
|
||||
def test_conflict_with_overlapping_dates(self):
|
||||
|
||||
@@ -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)
|
||||
from `tabGL Entry`
|
||||
where party_type = %s and party=%s
|
||||
and is_cancelled = 0
|
||||
group by company""", (party_type, party)))
|
||||
|
||||
for d in companies:
|
||||
|
||||
@@ -397,6 +397,7 @@ def get_chart_data(filters, columns, data):
|
||||
{'name': 'Budget', 'chartType': 'bar', 'values': budget_values},
|
||||
{'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values}
|
||||
]
|
||||
}
|
||||
},
|
||||
'type' : 'bar'
|
||||
}
|
||||
|
||||
|
||||
@@ -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.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
|
||||
acc.account_name, acc.account_number
|
||||
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s
|
||||
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
|
||||
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
|
||||
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
|
||||
{
|
||||
|
||||
@@ -48,17 +48,18 @@ def validate_filters(filters, account_details):
|
||||
|
||||
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"))))
|
||||
|
||||
for account in filters.account:
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
if filters.get('account'):
|
||||
filters.account = frappe.parse_json(filters.get('account'))
|
||||
for account in filters.account:
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
if (filters.get("account") and filters.get("group_by") == _('Group by Account')
|
||||
and account_details[filters.account].is_group == 0):
|
||||
frappe.throw(_("Can not filter based on Account, if grouped by Account"))
|
||||
if (filters.get("account") and filters.get("group_by") == _('Group by Account')):
|
||||
filters.account = frappe.parse_json(filters.get('account'))
|
||||
for account in filters.account:
|
||||
if account_details[account].is_group == 0:
|
||||
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
||||
|
||||
if (filters.get("voucher_no")
|
||||
and filters.get("group_by") in [_('Group by Voucher')]):
|
||||
|
||||
@@ -75,7 +75,8 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
|
||||
select voucher_no, credit
|
||||
from `tabGL Entry`
|
||||
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_credit_amount = flt(sum(d.credit for d in entries))
|
||||
|
||||
@@ -472,7 +472,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
"total_amount": d.grand_total,
|
||||
"outstanding_amount": d.outstanding_amount,
|
||||
"allocated_amount": d.allocated_amount,
|
||||
"exchange_rate": d.exchange_rate
|
||||
"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:
|
||||
@@ -498,12 +499,15 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
payment_entry.set_amounts()
|
||||
|
||||
if d.difference_amount and d.difference_account:
|
||||
payment_entry.set_gain_or_loss(account_details={
|
||||
account_details = {
|
||||
'account': d.difference_account,
|
||||
'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company',
|
||||
payment_entry.company, "cost_center"),
|
||||
'amount': d.difference_amount
|
||||
})
|
||||
payment_entry.company, "cost_center")
|
||||
}
|
||||
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:
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
@@ -784,7 +788,7 @@ def get_children(doctype, parent, company, is_root=False):
|
||||
return acc
|
||||
|
||||
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")
|
||||
if not company:
|
||||
|
||||
@@ -97,6 +97,9 @@
|
||||
"is_fixed_asset",
|
||||
"item_tax_rate",
|
||||
"section_break_72",
|
||||
"production_plan",
|
||||
"production_plan_item",
|
||||
"production_plan_sub_assembly_item",
|
||||
"page_break"
|
||||
],
|
||||
"fields": [
|
||||
@@ -803,13 +806,37 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 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,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-22 11:46:12.357435",
|
||||
"modified": "2021-06-28 19:22:22.715365",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -124,6 +124,8 @@ class AccountsController(TransactionBase):
|
||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||
self.set_advances()
|
||||
|
||||
self.set_advance_gain_or_loss()
|
||||
|
||||
if self.is_return:
|
||||
self.validate_qty()
|
||||
else:
|
||||
@@ -584,15 +586,18 @@ class AccountsController(TransactionBase):
|
||||
allocated_amount = min(amount - advance_allocated, d.amount)
|
||||
advance_allocated += flt(allocated_amount)
|
||||
|
||||
self.append("advances", {
|
||||
advance_row = {
|
||||
"doctype": self.doctype + " Advance",
|
||||
"reference_type": d.reference_type,
|
||||
"reference_name": d.reference_name,
|
||||
"reference_row": d.reference_row,
|
||||
"remarks": d.remarks,
|
||||
"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):
|
||||
if self.doctype == "Sales Invoice":
|
||||
@@ -650,6 +655,66 @@ class AccountsController(TransactionBase):
|
||||
"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.")
|
||||
.format(d.reference_name, d.against_order))
|
||||
|
||||
def set_advance_gain_or_loss(self):
|
||||
if 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:
|
||||
party = self.supplier if self.get('doctype') == 'Purchase Invoice' else self.customer
|
||||
party_account = self.credit_to if self.get('doctype') == 'Purchase Invoice' else self.debit_to
|
||||
party_type = "Supplier" if self.get('doctype') == 'Purchase Invoice' else "Customer"
|
||||
|
||||
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
|
||||
account_currency = get_account_currency(gain_loss_account)
|
||||
if account_currency != self.company_currency:
|
||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
|
||||
|
||||
# for purchase
|
||||
dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit'
|
||||
# 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):
|
||||
"""
|
||||
Links invoice and advance voucher:
|
||||
@@ -690,7 +755,9 @@ class AccountsController(TransactionBase):
|
||||
if self.party_account_currency != self.company_currency else 1),
|
||||
'grand_total': (self.base_grand_total
|
||||
if self.party_account_currency == self.company_currency else self.grand_total),
|
||||
'outstanding_amount': self.outstanding_amount
|
||||
'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)
|
||||
|
||||
@@ -751,11 +818,11 @@ class AccountsController(TransactionBase):
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
|
||||
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"
|
||||
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
|
||||
unallocated_amount = tax.tax_amount - tax.allocated_amount
|
||||
@@ -1289,6 +1356,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"
|
||||
currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency"
|
||||
payment_type = "Receive" if party_type == "Customer" else "Pay"
|
||||
exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate"
|
||||
|
||||
payment_entries_against_order, unallocated_payment_entries = [], []
|
||||
limit_cond = "limit %s" % limit if limit else ""
|
||||
|
||||
@@ -1305,27 +1374,28 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
||||
"Payment Entry" as reference_type, t1.name as reference_name,
|
||||
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
|
||||
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
|
||||
where
|
||||
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 t2.reference_doctype = %s {2}
|
||||
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,
|
||||
order_doctype] + order_list, as_dict=1)
|
||||
|
||||
if include_unallocated:
|
||||
unallocated_payment_entries = frappe.db.sql("""
|
||||
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`
|
||||
where
|
||||
{0} = %s and party_type = %s and party = %s and payment_type = %s
|
||||
and docstatus = 1 and unallocated_amount > 0
|
||||
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)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
|
||||
from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
@@ -356,42 +356,68 @@ class StockController(AccountsController):
|
||||
}, update_modified)
|
||||
|
||||
def validate_inspection(self):
|
||||
'''Checks if quality inspection is set for Items that require inspection.
|
||||
On submit, throw an exception'''
|
||||
inspection_required_fieldname = None
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
inspection_required_fieldname = "inspection_required_before_purchase"
|
||||
elif self.doctype in ["Delivery Note", "Sales Invoice"]:
|
||||
inspection_required_fieldname = "inspection_required_before_delivery"
|
||||
"""Checks if quality inspection is set/ is valid for Items that require inspection."""
|
||||
inspection_fieldname_map = {
|
||||
"Purchase Receipt": "inspection_required_before_purchase",
|
||||
"Purchase Invoice": "inspection_required_before_purchase",
|
||||
"Sales Invoice": "inspection_required_before_delivery",
|
||||
"Delivery Note": "inspection_required_before_delivery"
|
||||
}
|
||||
inspection_required_fieldname = inspection_fieldname_map.get(self.doctype)
|
||||
|
||||
# return if inspection is not required on document level
|
||||
if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or
|
||||
(self.doctype == "Stock Entry" and not self.inspection_required) or
|
||||
(self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)):
|
||||
return
|
||||
|
||||
for d in self.get('items'):
|
||||
qa_required = False
|
||||
if (inspection_required_fieldname and not d.quality_inspection and
|
||||
frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)):
|
||||
qa_required = True
|
||||
elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse:
|
||||
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)
|
||||
for row in self.get('items'):
|
||||
qi_required = False
|
||||
if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)):
|
||||
qi_required = True
|
||||
elif self.doctype == "Stock Entry" and row.t_warehouse:
|
||||
qi_required = True # inward stock needs inspection
|
||||
|
||||
if qa_doc.status != 'Accepted':
|
||||
frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
|
||||
.format(d.idx, d.item_code), QualityInspectionRejectedError)
|
||||
elif qa_required :
|
||||
action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted
|
||||
if self.docstatus==1 and action == 'Stop':
|
||||
frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)),
|
||||
exc=QualityInspectionRequiredError)
|
||||
else:
|
||||
frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code)))
|
||||
if qi_required: # validate row only if inspection is required on item level
|
||||
self.validate_qi_presence(row)
|
||||
if self.docstatus == 1:
|
||||
self.validate_qi_submission(row)
|
||||
self.validate_qi_rejection(row)
|
||||
|
||||
def validate_qi_presence(self, row):
|
||||
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
|
||||
if not row.quality_inspection:
|
||||
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):
|
||||
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))
|
||||
@@ -497,9 +523,6 @@ class StockController(AccountsController):
|
||||
})
|
||||
if future_sle_exists(args):
|
||||
create_repost_item_valuation_entry(args)
|
||||
elif not is_reposting_pending():
|
||||
check_if_stock_and_account_balance_synced(self.posting_date,
|
||||
self.company, self.doctype, self.name)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-01-28 16:16:45.447213",
|
||||
"modified": "2021-06-29 18:27:02.832979",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Appointment",
|
||||
@@ -153,6 +153,18 @@
|
||||
"role": "Sales User",
|
||||
"share": 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,
|
||||
|
||||
@@ -168,12 +168,13 @@ class Lead(SellingController):
|
||||
if self.phone:
|
||||
contact.append("phone_nos", {
|
||||
"phone": self.phone,
|
||||
"is_primary": 1
|
||||
"is_primary_phone": 1
|
||||
})
|
||||
|
||||
if self.mobile_no:
|
||||
contact.append("phone_nos", {
|
||||
"phone": self.mobile_no
|
||||
"phone": self.mobile_no,
|
||||
"is_primary_mobile_no":1
|
||||
})
|
||||
|
||||
contact.insert(ignore_permissions=True)
|
||||
|
||||
@@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program):
|
||||
student = get_current_student()
|
||||
course_enrollment = get_enrollment("course", course, student.name)
|
||||
if not course_enrollment:
|
||||
program_enrollment = get_enrollment('program', program, student.name)
|
||||
program_enrollment = get_enrollment('program', program.name, student.name)
|
||||
if not program_enrollment:
|
||||
frappe.throw(_("You are not enrolled in program {0}").format(program))
|
||||
return
|
||||
return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name))
|
||||
return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name))
|
||||
else:
|
||||
return frappe.get_doc('Course Enrollment', course_enrollment)
|
||||
|
||||
|
||||
@@ -7,16 +7,21 @@ import frappe
|
||||
import unittest
|
||||
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.erpnext_integrations.utils import create_mode_of_payment
|
||||
|
||||
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):
|
||||
frappe.db.sql('delete from `tabMpesa Settings`')
|
||||
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
|
||||
|
||||
def test_creation_of_payment_gateway(self):
|
||||
create_mpesa_settings(payment_gateway_name="_Test")
|
||||
|
||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test")
|
||||
mode_of_payment = create_mode_of_payment('Mpesa-_Test', payment_type="Phone")
|
||||
self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"}))
|
||||
self.assertTrue(mode_of_payment.name)
|
||||
self.assertEqual(mode_of_payment.type, "Phone")
|
||||
@@ -47,7 +52,6 @@ class TestMpesaSettings(unittest.TestCase):
|
||||
integration_request.delete()
|
||||
|
||||
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")
|
||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
|
||||
@@ -90,7 +94,6 @@ class TestMpesaSettings(unittest.TestCase):
|
||||
pos_invoice.delete()
|
||||
|
||||
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")
|
||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
|
||||
@@ -141,7 +144,6 @@ class TestMpesaSettings(unittest.TestCase):
|
||||
pos_invoice.delete()
|
||||
|
||||
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")
|
||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||
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
|
||||
doctype="Mpesa Settings",
|
||||
sandbox=1,
|
||||
payment_gateway_name=payment_gateway_name,
|
||||
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
|
||||
consumer_secret="VI1oS3oBGPJfh3JyvLHw",
|
||||
|
||||
@@ -52,7 +52,8 @@ def create_mode_of_payment(gateway, payment_type="General"):
|
||||
"payment_gateway": gateway
|
||||
}, ['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({
|
||||
"doctype": "Mode of Payment",
|
||||
"mode_of_payment": gateway,
|
||||
@@ -66,6 +67,10 @@ def create_mode_of_payment(gateway, payment_type="General"):
|
||||
})
|
||||
mode_of_payment.insert(ignore_permissions=True)
|
||||
|
||||
return mode_of_payment
|
||||
elif mode_of_payment:
|
||||
return frappe.get_doc("Mode of Payment", mode_of_payment)
|
||||
|
||||
def get_tracking_url(carrier, tracking_number):
|
||||
# Return the formatted Tracking URL.
|
||||
tracking_url = ''
|
||||
|
||||
@@ -157,6 +157,7 @@ website_route_rules = [
|
||||
"parents": [{"label": _("Material Request"), "route": "material-requests"}]
|
||||
}
|
||||
},
|
||||
{"from_route": "/project", "to_route": "Project"}
|
||||
]
|
||||
|
||||
standard_portal_menu_items = [
|
||||
|
||||
@@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) {
|
||||
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
|
||||
return{
|
||||
query: "erpnext.controllers.queries.employee_query"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class Attendance(Document):
|
||||
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
|
||||
self.validate_attendance_date()
|
||||
self.validate_duplicate_record()
|
||||
self.validate_employee_status()
|
||||
self.check_leave_record()
|
||||
|
||||
def validate_attendance_date(self):
|
||||
@@ -38,6 +39,10 @@ class Attendance(Document):
|
||||
frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
|
||||
frappe.bold(self.employee), frappe.bold(self.attendance_date)))
|
||||
|
||||
def validate_employee_status(self):
|
||||
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
|
||||
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
|
||||
|
||||
def check_leave_record(self):
|
||||
leave_record = frappe.db.sql("""
|
||||
select leave_type, half_day, half_day_date
|
||||
|
||||
@@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = {
|
||||
label: __('For Employee'),
|
||||
fieldtype: 'Link',
|
||||
options: 'Employee',
|
||||
get_query: () => {
|
||||
return {query: "erpnext.controllers.queries.employee_query"}
|
||||
},
|
||||
reqd: 1,
|
||||
onchange: function() {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 1);
|
||||
|
||||
@@ -72,7 +72,8 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
def test_expense_claim_gl_entry(self):
|
||||
payable_account = get_payable_account(company_name)
|
||||
taxes = generate_taxes()
|
||||
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _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()
|
||||
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||
@@ -82,7 +83,7 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
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],
|
||||
["Travel Expenses - _TC4", 200.0, 0.0]
|
||||
])
|
||||
@@ -145,7 +146,7 @@ def generate_taxes():
|
||||
parent_account = frappe.db.get_value('Account',
|
||||
{'company': company_name, 'is_group':1, 'account_type': 'Tax'},
|
||||
'name')
|
||||
account = create_account(company=company_name, account_name="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':[{
|
||||
"account_head": account,
|
||||
"rate": 0,
|
||||
|
||||
@@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', {
|
||||
frappe.set_route("List", "Training Feedback");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
frm.events.set_employee_query(frm);
|
||||
},
|
||||
|
||||
frappe.ui.form.on("Training Event Employee", {
|
||||
employee: function (frm) {
|
||||
set_employee_query: function(frm) {
|
||||
let emp = [];
|
||||
for (let d in frm.doc.employees) {
|
||||
if (frm.doc.employees[d].employee) {
|
||||
@@ -34,9 +33,17 @@ frappe.ui.form.on("Training Event Employee", {
|
||||
frm.set_query("employee", "employees", function () {
|
||||
return {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Employee",
|
||||
"no_copy": 1,
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
@@ -68,7 +69,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-21 12:41:59.336237",
|
||||
"modified": "2021-07-02 17:20:27.630176",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Training Event Employee",
|
||||
|
||||
@@ -28,7 +28,8 @@ frappe.ui.form.on('Loan', {
|
||||
frm.set_query("loan_type", function () {
|
||||
return {
|
||||
"filters": {
|
||||
"docstatus": 1
|
||||
"docstatus": 1,
|
||||
"company": frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -14,6 +14,13 @@ frappe.ui.form.on('Loan Application', {
|
||||
refresh: function(frm) {
|
||||
frm.trigger("toggle_fields");
|
||||
frm.trigger("add_toolbar_buttons");
|
||||
frm.set_query('loan_type', () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
repayment_method: function(frm) {
|
||||
frm.doc.repayment_amount = frm.doc.repayment_periods = ""
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
"no_copy": 1,
|
||||
"options": "Loan Security Pledge",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan_application.applicant",
|
||||
@@ -45,47 +47,63 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_security_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Loan Security Details"
|
||||
"label": "Loan Security Details",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan",
|
||||
"options": "Loan"
|
||||
"options": "Loan",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_application",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loan Application",
|
||||
"options": "Loan Application"
|
||||
"options": "Loan Application",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_security_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Security Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "maximum_loan_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Maximum Loan Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Loan Details"
|
||||
"label": "Loan Details",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "Requested",
|
||||
@@ -94,37 +112,49 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Requested\nUnpledged\nPledged\nPartially Pledged",
|
||||
"read_only": 1
|
||||
"options": "Requested\nUnpledged\nPledged\nPartially Pledged\nCancelled",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pledge_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Pledge Time",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "securities",
|
||||
"fieldtype": "Table",
|
||||
"label": "Securities",
|
||||
"options": "Pledge",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_10",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
"label": "Totals",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant_type",
|
||||
@@ -132,35 +162,45 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "more_information_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Information"
|
||||
"label": "More Information",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "reference_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference No"
|
||||
"label": "Reference No",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description"
|
||||
"label": "Description",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 18:23:16.953305",
|
||||
"modified": "2021-06-29 17:15:16.082256",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Security Pledge",
|
||||
|
||||
@@ -23,6 +23,12 @@ class LoanSecurityPledge(Document):
|
||||
update_shortfall_status(self.loan, self.total_security_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):
|
||||
security_list = []
|
||||
for security in self.securities:
|
||||
@@ -36,7 +42,7 @@ class LoanSecurityPledge(Document):
|
||||
existing_pledge = ''
|
||||
|
||||
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:
|
||||
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.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'])
|
||||
|
||||
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))
|
||||
if cancel:
|
||||
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))
|
||||
|
||||
@@ -13,7 +13,7 @@ frappe.ui.form.on('Blanket Order', {
|
||||
|
||||
refresh: function(frm) {
|
||||
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() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2018-05-24 07:18:08.256060",
|
||||
"doctype": "DocType",
|
||||
@@ -79,6 +80,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Date",
|
||||
@@ -129,8 +131,10 @@
|
||||
"label": "Terms and Conditions Details"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"modified": "2019-11-18 19:37:37.151686",
|
||||
"links": [],
|
||||
"modified": "2021-06-29 00:30:30.621636",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Blanket Order",
|
||||
|
||||
@@ -325,8 +325,7 @@ frappe.ui.form.on("BOM", {
|
||||
freeze: true,
|
||||
args: {
|
||||
update_parent: true,
|
||||
from_child_bom:false,
|
||||
save: frm.doc.docstatus === 1 ? true : false
|
||||
from_child_bom:false
|
||||
},
|
||||
callback: function(r) {
|
||||
refresh_field("items");
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"materials_section",
|
||||
"inspection_required",
|
||||
"quality_inspection_template",
|
||||
"column_break_31",
|
||||
"bom_level",
|
||||
"section_break_33",
|
||||
"items",
|
||||
"scrap_section",
|
||||
"scrap_items",
|
||||
@@ -513,6 +516,22 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 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",
|
||||
@@ -520,7 +539,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-16 12:25:09.081968",
|
||||
"modified": "2021-05-16 12:25:09.081968",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from typing import List
|
||||
from collections import deque
|
||||
import frappe, erpnext
|
||||
from frappe.utils import cint, cstr, flt, today
|
||||
from frappe import _
|
||||
@@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
import functools
|
||||
|
||||
from six import string_types
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
}
|
||||
|
||||
|
||||
class BOMTree:
|
||||
"""Full tree representation of a BOM"""
|
||||
|
||||
# specifying the attributes to save resources
|
||||
# ref: https://docs.python.org/3/reference/datamodel.html#slots
|
||||
__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
|
||||
|
||||
def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None:
|
||||
self.name = name # name of node, BOM number if is_bom else item_code
|
||||
self.child_items: List["BOMTree"] = [] # list of child items
|
||||
self.is_bom = is_bom # true if the node is a BOM and not a leaf item
|
||||
self.item_code: str = None # item_code associated with node
|
||||
self.qty = qty # required unit quantity to make one unit of parent item.
|
||||
self.exploded_qty = exploded_qty # total exploded qty required for making root of tree.
|
||||
if not self.is_bom:
|
||||
self.item_code = self.name
|
||||
else:
|
||||
self.__create_tree()
|
||||
|
||||
def __create_tree(self):
|
||||
bom = frappe.get_cached_doc("BOM", self.name)
|
||||
self.item_code = bom.item
|
||||
|
||||
for item in bom.get("items", []):
|
||||
qty = item.qty / bom.quantity # quantity per unit
|
||||
exploded_qty = self.exploded_qty * qty
|
||||
if item.bom_no:
|
||||
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
|
||||
self.child_items.append(child)
|
||||
else:
|
||||
self.child_items.append(
|
||||
BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty)
|
||||
)
|
||||
|
||||
def level_order_traversal(self) -> List["BOMTree"]:
|
||||
"""Get level order traversal of tree.
|
||||
E.g. for following tree the traversal will return list of nodes in order from top to bottom.
|
||||
BOM:
|
||||
- SubAssy1
|
||||
- item1
|
||||
- item2
|
||||
- SubAssy2
|
||||
- item3
|
||||
- item4
|
||||
|
||||
returns = [SubAssy1, item1, item2, SubAssy2, item3, item4]
|
||||
"""
|
||||
traversal = []
|
||||
q = deque()
|
||||
q.append(self)
|
||||
|
||||
while q:
|
||||
node = q.popleft()
|
||||
|
||||
for child in node.child_items:
|
||||
traversal.append(child)
|
||||
q.append(child)
|
||||
|
||||
return traversal
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}"
|
||||
f" exploded_qty: {self.exploded_qty}"
|
||||
)
|
||||
|
||||
def __repr__(self, level: int = 0) -> str:
|
||||
rep = "┃ " * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n"
|
||||
for child in self.child_items:
|
||||
rep += child.__repr__(level=level + 1)
|
||||
return rep
|
||||
|
||||
class BOM(WebsiteGenerator):
|
||||
website = frappe._dict(
|
||||
# page_title_field = "item_name",
|
||||
@@ -83,6 +155,7 @@ class BOM(WebsiteGenerator):
|
||||
self.update_stock_qty()
|
||||
self.validate_scrap_items()
|
||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
|
||||
self.set_bom_level()
|
||||
|
||||
|
||||
def get_context(self, context):
|
||||
@@ -154,7 +227,7 @@ class BOM(WebsiteGenerator):
|
||||
if not args:
|
||||
args = frappe.form_dict.get('args')
|
||||
|
||||
if isinstance(args, string_types):
|
||||
if isinstance(args, str):
|
||||
import json
|
||||
args = json.loads(args)
|
||||
|
||||
@@ -602,6 +675,7 @@ class BOM(WebsiteGenerator):
|
||||
if not d.batch_size or d.batch_size <= 0:
|
||||
d.batch_size = 1
|
||||
|
||||
|
||||
def validate_scrap_items(self):
|
||||
for item in self.scrap_items:
|
||||
if item.item_code == self.item and not item.is_process_loss:
|
||||
@@ -628,6 +702,24 @@ class BOM(WebsiteGenerator):
|
||||
' ' + _('set to 0 because') + ' ' +
|
||||
frappe.bold(_('Is Process Loss')) + ' ' + _('is checked'))
|
||||
|
||||
def get_tree_representation(self) -> BOMTree:
|
||||
"""Get a complete tree representation preserving order of child items."""
|
||||
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):
|
||||
if bom_doc.rm_cost_as_per == 'Valuation Rate':
|
||||
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)
|
||||
@@ -650,7 +742,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_factor": args.get("conversion_factor") or 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"))
|
||||
out = frappe._dict()
|
||||
@@ -814,7 +907,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
|
||||
frappe.form_dict.parent = 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)
|
||||
|
||||
bom_items = frappe.get_all('BOM Item',
|
||||
@@ -825,7 +918,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
|
||||
item_names = tuple(d.get('item_code') for d in bom_items)
|
||||
|
||||
items = frappe.get_list('Item',
|
||||
fields=['image', 'description', 'name', 'stock_uom', 'item_name'],
|
||||
fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'],
|
||||
filters=[['name', 'in', item_names]]) # to get only required item dicts
|
||||
|
||||
for bom_item in bom_items:
|
||||
@@ -838,6 +931,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
|
||||
|
||||
bom_item.parent_bom_qty = bom_doc.quantity
|
||||
bom_item.expandable = 0 if bom_item.value in ('', None) else 1
|
||||
bom_item.image = frappe.db.escape(bom_item.image)
|
||||
|
||||
return bom_items
|
||||
|
||||
@@ -1054,6 +1148,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
|
||||
},
|
||||
'BOM Item': {
|
||||
'doctype': 'BOM Item',
|
||||
# stop get_mapped_doc copying parent bom_no to children
|
||||
'field_no_map': ['bom_no'],
|
||||
'condition': lambda doc: doc.has_variants == 0
|
||||
},
|
||||
}, target_doc, postprocess)
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
<div style="padding: 15px;">
|
||||
{% if data.image %}
|
||||
<img class="responsive" src={{ data.image }}>
|
||||
<hr style="margin: 15px -15px;">
|
||||
{% endif %}
|
||||
<h4>
|
||||
{{ __("Description") }}
|
||||
</h4>
|
||||
<div style="padding-top: 10px;">
|
||||
{{ data.description }}
|
||||
<div class="row mb-5">
|
||||
<div class="col-md-5" style="max-height: 500px">
|
||||
{% if data.image %}
|
||||
<div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
|
||||
<img class="responsive" src={{ data.image }}>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<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>
|
||||
<hr style="margin: 15px -15px;">
|
||||
<p>
|
||||
|
||||
@@ -64,7 +64,7 @@ frappe.treeview_settings["BOM"] = {
|
||||
if(node.is_root && node.data.value!="BOM") {
|
||||
frappe.model.with_doc("BOM", node.data.value, function() {
|
||||
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 || "";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from collections import deque
|
||||
import unittest
|
||||
import frappe
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.test_runner import make_test_records
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
|
||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
||||
from six import string_types
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
||||
@@ -227,6 +226,7 @@ class TestBOM(unittest.TestCase):
|
||||
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
|
||||
self.assertEqual(bom_items, supplied_items)
|
||||
|
||||
|
||||
def test_bom_with_process_loss_item(self):
|
||||
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
||||
|
||||
@@ -260,11 +260,84 @@ class TestBOM(unittest.TestCase):
|
||||
# FG Items in Scrap/Loss Table should have Is Process Loss set
|
||||
self.assertRaises(frappe.ValidationError, bom_doc.submit)
|
||||
|
||||
def test_bom_tree_representation(self):
|
||||
bom_tree = {
|
||||
"Assembly": {
|
||||
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
|
||||
"SubAssembly2": {"ChildPart3": {}},
|
||||
"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
|
||||
"ChildPart5": {},
|
||||
"ChildPart6": {},
|
||||
"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
|
||||
}
|
||||
}
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
created_tree = parent_bom.get_tree_representation()
|
||||
|
||||
reqd_order = level_order_traversal(bom_tree)[1:] # skip first item
|
||||
created_order = created_tree.level_order_traversal()
|
||||
|
||||
self.assertEqual(len(reqd_order), len(created_order))
|
||||
|
||||
for reqd_item, created_item in zip(reqd_order, created_order):
|
||||
self.assertEqual(reqd_item, created_item.item_code)
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
def level_order_traversal(node):
|
||||
traversal = []
|
||||
q = deque()
|
||||
q.append(node)
|
||||
|
||||
while q:
|
||||
node = q.popleft()
|
||||
|
||||
for node_name, subtree in node.items():
|
||||
traversal.append(node_name)
|
||||
q.append(subtree)
|
||||
|
||||
return traversal
|
||||
|
||||
def create_nested_bom(tree, prefix="_Test bom "):
|
||||
""" Helper function to create a simple nested bom from tree describing item names. (along with required items)
|
||||
"""
|
||||
|
||||
def create_items(bom_tree):
|
||||
for item_code, subtree in bom_tree.items():
|
||||
bom_item_code = prefix + item_code
|
||||
if not frappe.db.exists("Item", bom_item_code):
|
||||
frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert()
|
||||
create_items(subtree)
|
||||
create_items(tree)
|
||||
|
||||
def dfs(tree, node):
|
||||
"""naive implementation for searching right subtree"""
|
||||
for node_name, subtree in tree.items():
|
||||
if node_name == node:
|
||||
return subtree
|
||||
else:
|
||||
result = dfs(subtree, node)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
order_of_creating_bom = reversed(level_order_traversal(tree))
|
||||
|
||||
for item in order_of_creating_bom:
|
||||
child_items = dfs(tree, item)
|
||||
if child_items:
|
||||
bom_item_code = prefix + item
|
||||
bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
|
||||
for child_item in child_items.keys():
|
||||
bom.append("items", {"item_code": prefix + child_item})
|
||||
bom.insert()
|
||||
bom.submit()
|
||||
|
||||
return bom # parent bom is last bom
|
||||
|
||||
|
||||
def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None):
|
||||
if warehouse_list and isinstance(warehouse_list, string_types):
|
||||
if warehouse_list and isinstance(warehouse_list, str):
|
||||
warehouse_list = [warehouse_list]
|
||||
|
||||
if not warehouse_list:
|
||||
|
||||
@@ -192,15 +192,20 @@ class JobCard(Document):
|
||||
"completed_qty": args.get("completed_qty") or 0.0
|
||||
})
|
||||
elif args.get("start_time"):
|
||||
for name in employees:
|
||||
self.append("time_logs", {
|
||||
"from_time": get_datetime(args.get("start_time")),
|
||||
"employee": name.get('employee'),
|
||||
"operation": args.get("sub_operation"),
|
||||
"completed_qty": 0.0
|
||||
})
|
||||
new_args = {
|
||||
"from_time": get_datetime(args.get("start_time")),
|
||||
"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)
|
||||
|
||||
if self.status == "On Hold":
|
||||
@@ -208,6 +213,9 @@ class JobCard(Document):
|
||||
|
||||
self.save()
|
||||
|
||||
def add_start_time_log(self, args):
|
||||
self.append("time_logs", args)
|
||||
|
||||
def set_employees(self, employees):
|
||||
for name in employees:
|
||||
self.append('employee', {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
frappe.ui.form.on('Production Plan', {
|
||||
setup: function(frm) {
|
||||
frm.custom_make_buttons = {
|
||||
'Work Order': 'Work Order',
|
||||
'Work Order': 'Work Order / Subcontract PO',
|
||||
'Material Request': 'Material Request',
|
||||
};
|
||||
|
||||
@@ -68,17 +68,13 @@ frappe.ui.form.on('Production Plan', {
|
||||
frm.trigger("show_progress");
|
||||
|
||||
if (frm.doc.status !== "Completed") {
|
||||
if (frm.doc.po_items && frm.doc.status !== "Closed") {
|
||||
frm.add_custom_button(__("Work Order"), ()=> {
|
||||
frm.trigger("make_work_order");
|
||||
}, __('Create'));
|
||||
}
|
||||
frm.add_custom_button(__("Work Order Tree"), ()=> {
|
||||
frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name});
|
||||
}, __('View'));
|
||||
|
||||
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'));
|
||||
}
|
||||
frm.add_custom_button(__("Production Plan Summary"), ()=> {
|
||||
frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name});
|
||||
}, __('View'));
|
||||
|
||||
if (frm.doc.status === "Closed") {
|
||||
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);
|
||||
}, __("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) {
|
||||
if (!frm.doc.for_warehouse) {
|
||||
frappe.throw(__("Select warehouse for material requests"));
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
"po_items",
|
||||
"section_break_25",
|
||||
"prod_plan_references",
|
||||
"section_break_24",
|
||||
"get_sub_assembly_items",
|
||||
"sub_assembly_items",
|
||||
"material_request_planning",
|
||||
"include_non_stock_items",
|
||||
"include_subcontracted_items",
|
||||
@@ -187,7 +190,7 @@
|
||||
"depends_on": "get_items_from",
|
||||
"fieldname": "get_items",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Items For Work Order"
|
||||
"label": "Get Finished Goods for Manufacture"
|
||||
},
|
||||
{
|
||||
"fieldname": "po_items",
|
||||
@@ -199,7 +202,7 @@
|
||||
{
|
||||
"fieldname": "material_request_planning",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Material Request Planning"
|
||||
"label": "Material Requirement Planning"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@@ -237,12 +240,13 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_27",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "mr_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Material Request Plan Item",
|
||||
"label": "Raw Materials",
|
||||
"no_copy": 1,
|
||||
"options": "Material Request Plan Item"
|
||||
},
|
||||
@@ -337,13 +341,30 @@
|
||||
"hidden": 1,
|
||||
"label": "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",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-24 16:59:03.643211",
|
||||
"modified": "2021-06-28 20:00:33.905114",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe, json, copy
|
||||
from frappe import msgprint, _
|
||||
from six import string_types, iteritems
|
||||
from six import iteritems
|
||||
|
||||
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 erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||
@@ -349,49 +350,88 @@ class ProductionPlan(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_work_order(self):
|
||||
wo_list = []
|
||||
wo_list, po_list = [], []
|
||||
subcontracted_po = {}
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
if work_order:
|
||||
wo_list.append(work_order)
|
||||
|
||||
if item.get("make_work_order_for_sub_assembly_items"):
|
||||
work_orders = self.make_work_order_for_sub_assembly_items(item)
|
||||
wo_list.extend(work_orders)
|
||||
def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po):
|
||||
for row in self.sub_assembly_items:
|
||||
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
|
||||
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:
|
||||
wo_list = ["""<a href="/app/Form/Work Order/%s" target="_blank">%s</a>""" % \
|
||||
(p, p) for p in wo_list]
|
||||
msgprint(_("{0} created").format(comma_and(wo_list)))
|
||||
else :
|
||||
msgprint(_("No Work Orders created"))
|
||||
def prepare_args_for_sub_assembly_items(self, row, args):
|
||||
for field in ["production_item", "item_name", "qty", "fg_warehouse",
|
||||
"description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]:
|
||||
args[field] = row.get(field)
|
||||
|
||||
def make_work_order_for_sub_assembly_items(self, item):
|
||||
work_orders = []
|
||||
bom_data = {}
|
||||
|
||||
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
|
||||
args.update({
|
||||
"use_multi_level_bom": 0,
|
||||
"production_plan": self.name,
|
||||
"production_plan_sub_assembly_item": row.name
|
||||
})
|
||||
|
||||
def create_work_order(self, item):
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse
|
||||
@@ -476,9 +516,32 @@ class ProductionPlan(Document):
|
||||
else :
|
||||
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()
|
||||
def download_raw_materials(doc, warehouses=None):
|
||||
if isinstance(doc, string_types):
|
||||
if isinstance(doc, str):
|
||||
doc = frappe._dict(json.loads(doc))
|
||||
|
||||
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
|
||||
@@ -660,7 +723,7 @@ def get_sales_orders(self):
|
||||
|
||||
@frappe.whitelist()
|
||||
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))
|
||||
|
||||
company = frappe.db.escape(company)
|
||||
@@ -684,8 +747,11 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
|
||||
group by item_code, warehouse
|
||||
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
|
||||
|
||||
def get_warehouse_list(warehouses, warehouse_list=[]):
|
||||
if isinstance(warehouses, string_types):
|
||||
def get_warehouse_list(warehouses, warehouse_list=None):
|
||||
if not warehouse_list:
|
||||
warehouse_list = []
|
||||
|
||||
if isinstance(warehouses, str):
|
||||
warehouses = json.loads(warehouses)
|
||||
|
||||
for row in warehouses:
|
||||
@@ -697,7 +763,7 @@ def get_warehouse_list(warehouses, warehouse_list=[]):
|
||||
|
||||
@frappe.whitelist()
|
||||
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))
|
||||
|
||||
warehouse_list = []
|
||||
@@ -726,6 +792,9 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
|
||||
so_item_details = frappe._dict()
|
||||
for data in po_items:
|
||||
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
|
||||
data["include_exploded_items"] = 1
|
||||
|
||||
planned_qty = data.get('required_qty') or data.get('planned_qty')
|
||||
ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty
|
||||
warehouse = doc.get('for_warehouse')
|
||||
@@ -857,23 +926,28 @@ def get_item_data(item_code):
|
||||
# "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)
|
||||
for d in data:
|
||||
if d.expandable:
|
||||
key = (d.name, d.value)
|
||||
if key not in bom_data:
|
||||
bom_data.setdefault(key, {
|
||||
'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
|
||||
})
|
||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||
bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level")
|
||||
if d.value else 0)
|
||||
|
||||
bom_item = bom_data.get(key)
|
||||
bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||
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)
|
||||
|
||||
@@ -9,5 +9,9 @@ def get_data():
|
||||
'label': _('Transactions'),
|
||||
'items': ['Work Order', 'Material Request']
|
||||
},
|
||||
{
|
||||
'label': _('Subcontract'),
|
||||
'items': ['Purchase Order']
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -169,7 +169,7 @@ class TestProductionPlan(unittest.TestCase):
|
||||
pln.get_items()
|
||||
pln.submit()
|
||||
|
||||
self.assertTrue(pln.po_items[0].planned_qty, 3)
|
||||
self.assertTrue(pln.po_items[0].planned_qty, 3)
|
||||
|
||||
pln.make_work_order()
|
||||
work_order = frappe.db.get_value('Work Order', {
|
||||
@@ -193,10 +193,10 @@ class TestProductionPlan(unittest.TestCase):
|
||||
for so_item in so_items:
|
||||
so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty')
|
||||
self.assertEqual(so_wo_qty, 0.0)
|
||||
|
||||
|
||||
latest_plan = frappe.get_doc('Production Plan', pln.name)
|
||||
latest_plan.cancel()
|
||||
|
||||
|
||||
def test_pp_to_mr_customer_provided(self):
|
||||
#Material Request from Production Plan for Customer Provided
|
||||
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", {
|
||||
"item_code": item_code,
|
||||
"bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
|
||||
"planned_qty": 3,
|
||||
"make_work_order_for_sub_assembly_items": 1
|
||||
"planned_qty": 3
|
||||
})
|
||||
|
||||
pln.get_sub_assembly_items('In House')
|
||||
pln.submit()
|
||||
pln.make_work_order()
|
||||
|
||||
|
||||
@@ -9,18 +9,17 @@
|
||||
"include_exploded_items",
|
||||
"item_code",
|
||||
"bom_no",
|
||||
"planned_qty",
|
||||
"column_break_6",
|
||||
"make_work_order_for_sub_assembly_items",
|
||||
"planned_qty",
|
||||
"warehouse",
|
||||
"planned_start_date",
|
||||
"section_break_9",
|
||||
"pending_qty",
|
||||
"ordered_qty",
|
||||
"produced_qty",
|
||||
"column_break_17",
|
||||
"description",
|
||||
"stock_uom",
|
||||
"produced_qty",
|
||||
"reference_section",
|
||||
"sales_order",
|
||||
"sales_order_item",
|
||||
@@ -32,11 +31,10 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"default": "0",
|
||||
"columns": 1,
|
||||
"default": "1",
|
||||
"fieldname": "include_exploded_items",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Include Exploded Items"
|
||||
},
|
||||
{
|
||||
@@ -80,13 +78,6 @@
|
||||
"fieldname": "column_break_6",
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
@@ -218,7 +209,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-28 19:14:57.772123",
|
||||
"modified": "2021-06-28 18:31:06.822168",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Item",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -19,6 +19,7 @@
|
||||
"options": "Operation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Time in mins",
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Float",
|
||||
@@ -38,7 +39,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-07 18:09:18.005578",
|
||||
"modified": "2021-07-15 16:39:41.635362",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Sub Operation",
|
||||
|
||||
@@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase):
|
||||
ste.submit()
|
||||
stock_entries.append(ste)
|
||||
|
||||
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
|
||||
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc')
|
||||
self.assertEqual(len(job_cards), len(bom.operations))
|
||||
|
||||
for i, job_card in enumerate(job_cards):
|
||||
doc = frappe.get_doc("Job Card", job_card)
|
||||
doc.append("time_logs", {
|
||||
"from_time": add_to_date(None, i),
|
||||
"hours": 1,
|
||||
"to_time": add_to_date(None, i + 1),
|
||||
"completed_qty": doc.for_quantity
|
||||
})
|
||||
doc.time_logs[0].completed_qty = 1
|
||||
doc.submit()
|
||||
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
|
||||
@@ -518,6 +513,60 @@ class TestWorkOrder(unittest.TestCase):
|
||||
work_order1.save()
|
||||
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
|
||||
|
||||
def test_batch_size_for_fg_item(self):
|
||||
fg_item = "Test Batch Size Item For BOM 3"
|
||||
rm1 = "Test Batch Size Item RM 1 For BOM 3"
|
||||
|
||||
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0)
|
||||
for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]:
|
||||
item_args = {
|
||||
"include_item_in_manufacturing": 1,
|
||||
"is_stock_item": 1
|
||||
}
|
||||
|
||||
if item == fg_item:
|
||||
item_args['has_batch_no'] = 1
|
||||
item_args['create_new_batch'] = 1
|
||||
item_args['batch_number_series'] = 'TBSI3.#####'
|
||||
|
||||
make_item(item, item_args)
|
||||
|
||||
bom_name = frappe.db.get_value("BOM",
|
||||
{"item": fg_item, "is_active": 1, "with_operations": 1}, "name")
|
||||
|
||||
if not bom_name:
|
||||
bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True)
|
||||
bom.save()
|
||||
bom.submit()
|
||||
bom_name = bom.name
|
||||
|
||||
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1)
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
|
||||
for row in ste1.get('items'):
|
||||
if row.is_finished_item:
|
||||
self.assertEqual(row.item_code, fg_item)
|
||||
|
||||
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1)
|
||||
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1)
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
|
||||
for row in ste1.get('items'):
|
||||
if row.is_finished_item:
|
||||
self.assertEqual(row.item_code, fg_item)
|
||||
|
||||
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(),
|
||||
qty=30, do_not_save = True)
|
||||
work_order.batch_size = 10
|
||||
work_order.insert()
|
||||
work_order.submit()
|
||||
self.assertEqual(work_order.has_batch_no, 1)
|
||||
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30))
|
||||
for row in ste1.get('items'):
|
||||
if row.is_finished_item:
|
||||
self.assertEqual(row.item_code, fg_item)
|
||||
self.assertEqual(row.qty, 10)
|
||||
|
||||
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0)
|
||||
|
||||
def test_partial_material_consumption(self):
|
||||
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1)
|
||||
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)
|
||||
|
||||
@@ -64,11 +64,16 @@
|
||||
"description",
|
||||
"stock_uom",
|
||||
"column_break2",
|
||||
"references_section",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"sales_order_item",
|
||||
"column_break_61",
|
||||
"production_plan",
|
||||
"production_plan_item",
|
||||
"production_plan_sub_assembly_item",
|
||||
"parent_work_order",
|
||||
"bom_level",
|
||||
"product_bundle_item",
|
||||
"amended_from"
|
||||
],
|
||||
@@ -546,17 +551,26 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan_sub_assembly_item",
|
||||
"fieldtype": "Data",
|
||||
"label": "Production Plan Sub-assembly Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cogs",
|
||||
"idx": 1,
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-20 15:19:14.902699",
|
||||
"modified": "2021-06-28 16:19:14.902699",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
"nsm_parent_field": "parent_work_order",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
import math
|
||||
@@ -30,9 +29,6 @@ class ItemHasVariantError(frappe.ValidationError): pass
|
||||
class SerialNoQtyError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
form_grid_templates = {
|
||||
"operations": "templates/form_grid/work_order_grid.html"
|
||||
}
|
||||
|
||||
class WorkOrder(Document):
|
||||
def onload(self):
|
||||
@@ -243,7 +239,7 @@ class WorkOrder(Document):
|
||||
self.create_serial_no_batch_no()
|
||||
|
||||
def on_submit(self):
|
||||
if not self.wip_warehouse:
|
||||
if not self.wip_warehouse and not self.skip_transfer:
|
||||
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
|
||||
if not self.fg_warehouse:
|
||||
frappe.throw(_("For Warehouse is required before Submit"))
|
||||
@@ -472,46 +468,47 @@ class WorkOrder(Document):
|
||||
|
||||
def set_work_order_operations(self):
|
||||
"""Fetch operations from BOM and set in 'Work Order'"""
|
||||
self.set('operations', [])
|
||||
|
||||
if not self.bom_no:
|
||||
def _get_operations(bom_no, qty=1):
|
||||
return frappe.db.sql(
|
||||
f"""select
|
||||
operation, description, workstation, idx,
|
||||
base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins,
|
||||
"Pending" as status, parent as bom, batch_size, sequence_id
|
||||
from
|
||||
`tabBOM Operation`
|
||||
where
|
||||
parent = %s order by idx
|
||||
""", bom_no, as_dict=1)
|
||||
|
||||
|
||||
self.set('operations', [])
|
||||
if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'):
|
||||
return
|
||||
|
||||
if self.use_multi_level_bom:
|
||||
bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree()
|
||||
operations = []
|
||||
if not self.use_multi_level_bom:
|
||||
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
|
||||
operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
|
||||
else:
|
||||
bom_list = [self.bom_no]
|
||||
bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation()
|
||||
bom_traversal = list(reversed(bom_tree.level_order_traversal()))
|
||||
bom_traversal.append(bom_tree) # add operation on top level item last
|
||||
|
||||
for d in bom_traversal:
|
||||
if d.is_bom:
|
||||
operations.extend(_get_operations(d.name, qty=d.exploded_qty))
|
||||
|
||||
for correct_index, operation in enumerate(operations, start=1):
|
||||
operation.idx = correct_index
|
||||
|
||||
operations = frappe.db.sql("""
|
||||
select
|
||||
operation, description, workstation, idx,
|
||||
base_hour_rate as hour_rate, time_in_mins,
|
||||
"Pending" as status, parent as bom, batch_size, sequence_id
|
||||
from
|
||||
`tabBOM Operation`
|
||||
where
|
||||
parent in (%s) order by idx
|
||||
""" % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1)
|
||||
|
||||
self.set('operations', operations)
|
||||
|
||||
if self.use_multi_level_bom and self.get('operations') and self.get('items'):
|
||||
raw_material_operations = [d.operation for d in self.get('items')]
|
||||
operations = [d.operation for d in self.get('operations')]
|
||||
|
||||
for operation in raw_material_operations:
|
||||
if operation not in operations:
|
||||
self.append('operations', {
|
||||
'operation': operation
|
||||
})
|
||||
|
||||
self.calculate_time()
|
||||
|
||||
def calculate_time(self):
|
||||
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
|
||||
|
||||
for d in self.get("operations"):
|
||||
d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size))
|
||||
d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
|
||||
|
||||
self.calculate_operating_cost()
|
||||
|
||||
@@ -593,6 +590,7 @@ class WorkOrder(Document):
|
||||
def validate_operation_time(self):
|
||||
for d in self.operations:
|
||||
if not d.time_in_mins > 0:
|
||||
print(self.bom_no, self.production_item)
|
||||
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
|
||||
|
||||
def update_required_items(self):
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"actions": [],
|
||||
"creation": "2014-10-16 14:35:41.950175",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"details",
|
||||
@@ -49,6 +48,7 @@
|
||||
{
|
||||
"fieldname": "bom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "BOM",
|
||||
"no_copy": 1,
|
||||
"options": "BOM",
|
||||
@@ -68,6 +68,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"description": "Operation completed for how many finished goods?",
|
||||
"fieldname": "completed_qty",
|
||||
"fieldtype": "Float",
|
||||
@@ -77,6 +78,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
@@ -119,6 +121,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"description": "in Minutes",
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Float",
|
||||
@@ -205,7 +208,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-12 14:48:31.061286",
|
||||
"modified": "2021-06-24 14:36:12.835543",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Operation",
|
||||
@@ -214,4 +217,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,17 +20,20 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
||||
fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom'])
|
||||
|
||||
for item in exploded_items:
|
||||
print(item.bom_no, indent)
|
||||
item["indent"] = indent
|
||||
data.append({
|
||||
'item_code': item.item_code,
|
||||
'item_name': item.item_name,
|
||||
'indent': indent,
|
||||
'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level")
|
||||
if item.bom_no else ""),
|
||||
'bom': item.bom_no,
|
||||
'qty': item.qty * qty,
|
||||
'uom': item.uom,
|
||||
'description': item.description,
|
||||
'scrap': item.scrap
|
||||
})
|
||||
})
|
||||
if item.bom_no:
|
||||
get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty)
|
||||
|
||||
@@ -68,6 +71,12 @@ def get_columns():
|
||||
"fieldname": "uom",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": "BOM Level",
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "bom_level",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": "Standard Description",
|
||||
"fieldtype": "data",
|
||||
|
||||
@@ -70,12 +70,12 @@ def get_bom_stock(filters):
|
||||
ON bom_item.item_code = ledger.item_code
|
||||
{conditions}
|
||||
WHERE
|
||||
bom_item.parent = '{bom}' and bom_item.parenttype='BOM'
|
||||
bom_item.parent = {bom} and bom_item.parenttype='BOM'
|
||||
|
||||
GROUP BY bom_item.item_code""".format(
|
||||
qty_field=qty_field,
|
||||
table=table,
|
||||
conditions=conditions,
|
||||
bom=bom,
|
||||
bom=frappe.db.escape(bom),
|
||||
qty_to_produce=qty_to_produce or 1)
|
||||
)
|
||||
|
||||
@@ -68,6 +68,18 @@ frappe.query_reports["Job Card Summary"] = {
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Item', txt);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Workstation"),
|
||||
fieldname: "workstation",
|
||||
fieldtype: "Link",
|
||||
options: "Workstation"
|
||||
},
|
||||
{
|
||||
label: __("Operation"),
|
||||
fieldname: "operation",
|
||||
fieldtype: "Link",
|
||||
options: "Operation"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2020-04-20 12:00:21.436619",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letter_head": "Gadgets International",
|
||||
"modified": "2020-04-20 12:00:21.436619",
|
||||
"letter_head": "",
|
||||
"modified": "2020-12-30 11:49:21.713561",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Summary",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Production Plan Summary"] = {
|
||||
"filters": [
|
||||
{
|
||||
fieldname: "production_plan",
|
||||
label: __("Production Plan"),
|
||||
fieldtype: "Link",
|
||||
options: "Production Plan",
|
||||
reqd: 1,
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
"docstatus": 1
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
],
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (column.fieldname == "document_name") {
|
||||
var color = data.pending_qty > 0 ? 'red': 'green';
|
||||
value = `<a style='color:${color}' href="#Form/${data['document_type']}/${data['document_name']}" data-doctype="${data['document_type']}">${data['document_name']}</a>`;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2020-12-27 11:43:39.781793",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2020-12-27 11:43:42.677584",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Production Plan",
|
||||
"report_name": "Production Plan Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Manufacturing User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
data = get_data(filters)
|
||||
columns = get_column(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
|
||||
order_details = {}
|
||||
get_work_order_details(filters, order_details)
|
||||
get_purchase_order_details(filters, order_details)
|
||||
get_production_plan_item_details(filters, data, order_details)
|
||||
|
||||
return data
|
||||
|
||||
def get_production_plan_item_details(filters, data, order_details):
|
||||
itemwise_indent = {}
|
||||
|
||||
production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
|
||||
for row in production_plan_doc.po_items:
|
||||
work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name,
|
||||
"bom_no": row.bom_no, "production_item": row.item_code}, "name")
|
||||
|
||||
if row.item_code not in itemwise_indent:
|
||||
itemwise_indent.setdefault(row.item_code, {})
|
||||
|
||||
data.append({
|
||||
"indent": 0,
|
||||
"item_code": row.item_code,
|
||||
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
|
||||
"qty": row.planned_qty,
|
||||
"document_type": "Work Order",
|
||||
"document_name": work_order,
|
||||
"bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"),
|
||||
"produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"),
|
||||
"pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty"))
|
||||
})
|
||||
|
||||
get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details)
|
||||
|
||||
def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details):
|
||||
for item in production_plan_doc.sub_assembly_items:
|
||||
if row.name == item.production_plan_item:
|
||||
subcontracted_item = (item.type_of_manufacturing == 'Subcontract')
|
||||
|
||||
if subcontracted_item:
|
||||
docname = frappe.get_cached_value("Purchase Order Item",
|
||||
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent")
|
||||
else:
|
||||
docname = frappe.get_cached_value("Work Order",
|
||||
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name")
|
||||
|
||||
data.append({
|
||||
"indent": 1,
|
||||
"item_code": item.production_item,
|
||||
"item_name": item.item_name,
|
||||
"qty": item.qty,
|
||||
"document_type": "Work Order" if not subcontracted_item else "Purchase Order",
|
||||
"document_name": docname,
|
||||
"bom_level": item.bom_level,
|
||||
"produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"),
|
||||
"pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty"))
|
||||
})
|
||||
|
||||
def get_work_order_details(filters, order_details):
|
||||
for row in frappe.get_all("Work Order", filters = {"production_plan": filters.get("production_plan")},
|
||||
fields=["name", "produced_qty", "production_plan", "production_item"]):
|
||||
order_details.setdefault((row.name, row.production_item), row)
|
||||
|
||||
def get_purchase_order_details(filters, order_details):
|
||||
for row in frappe.get_all("Purchase Order Item", filters = {"production_plan": filters.get("production_plan")},
|
||||
fields=["parent", "received_qty as produced_qty", "item_code"]):
|
||||
order_details.setdefault((row.parent, row.item_code), row)
|
||||
|
||||
def get_column(filters):
|
||||
return [
|
||||
{
|
||||
"label": "Finished Good",
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "item_code",
|
||||
"width": 300,
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
"label": "Item Name",
|
||||
"fieldtype": "data",
|
||||
"fieldname": "item_name",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": "Document Type",
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "document_type",
|
||||
"width": 150,
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Document Name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldname": "document_name",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"label": "BOM Level",
|
||||
"fieldtype": "Int",
|
||||
"fieldname": "bom_level",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": "Order Qty",
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "qty",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"label": "Received Qty",
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "produced_qty",
|
||||
"width": 160
|
||||
},
|
||||
{
|
||||
"label": "Pending Qty",
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "pending_qty",
|
||||
"width": 110
|
||||
}
|
||||
]
|
||||
@@ -19,7 +19,7 @@ def execute(filters=None):
|
||||
return columns, data, None, chart_data
|
||||
|
||||
def get_data(filters):
|
||||
query_filters = {"docstatus": 1}
|
||||
query_filters = {"docstatus": ("<", 2)}
|
||||
|
||||
fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty",
|
||||
"planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"]
|
||||
@@ -62,7 +62,8 @@ def get_chart_based_on_status(data):
|
||||
"Not Started": 0,
|
||||
"In Process": 0,
|
||||
"Stopped": 0,
|
||||
"Completed": 0
|
||||
"Completed": 0,
|
||||
"Draft": 0
|
||||
}
|
||||
|
||||
for d in data:
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"razorpay_details_section",
|
||||
"subscription_id",
|
||||
"customer_id",
|
||||
"subscription_activated",
|
||||
"subscription_status",
|
||||
"column_break_21",
|
||||
"subscription_start",
|
||||
"subscription_end"
|
||||
@@ -151,12 +151,6 @@
|
||||
"fieldname": "column_break_21",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "subscription_activated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Subscription Activated"
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription_start",
|
||||
"fieldtype": "Date",
|
||||
@@ -166,11 +160,17 @@
|
||||
"fieldname": "subscription_end",
|
||||
"fieldtype": "Date",
|
||||
"label": "Subscription End"
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Subscription Status",
|
||||
"options": "\nActive\nHalted"
|
||||
}
|
||||
],
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2020-11-09 12:12:10.174647",
|
||||
"modified": "2021-07-11 14:27:26.368039",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Non Profit",
|
||||
"name": "Member",
|
||||
|
||||
@@ -84,7 +84,9 @@ def create_member(user_details):
|
||||
"email_id": user_details.email,
|
||||
"pan_number": user_details.pan or None,
|
||||
"membership_type": user_details.plan_id,
|
||||
"subscription_id": user_details.subscription_id or None
|
||||
"customer_id": user_details.customer_id or None,
|
||||
"subscription_id": user_details.subscription_id or None,
|
||||
"subscription_status": user_details.subscription_status or ""
|
||||
})
|
||||
|
||||
member.insert(ignore_permissions=True)
|
||||
|
||||
@@ -196,11 +196,14 @@ def make_invoice(membership, member, plan, settings):
|
||||
return invoice
|
||||
|
||||
|
||||
def get_member_based_on_subscription(subscription_id, email):
|
||||
members = frappe.get_all("Member", filters={
|
||||
"subscription_id": subscription_id,
|
||||
"email_id": email
|
||||
}, order_by="creation desc")
|
||||
def get_member_based_on_subscription(subscription_id, email=None, customer_id=None):
|
||||
filters = {"subscription_id": subscription_id}
|
||||
if email:
|
||||
filters.update({"email_id": email})
|
||||
if customer_id:
|
||||
filters.update({"customer_id": customer_id})
|
||||
|
||||
members = frappe.get_all("Member", filters=filters, order_by="creation desc")
|
||||
|
||||
try:
|
||||
return frappe.get_doc("Member", members[0]["name"])
|
||||
@@ -209,8 +212,6 @@ def get_member_based_on_subscription(subscription_id, email):
|
||||
|
||||
|
||||
def verify_signature(data, endpoint="Membership"):
|
||||
if frappe.flags.in_test or os.environ.get("CI"):
|
||||
return True
|
||||
signature = frappe.request.headers.get("X-Razorpay-Signature")
|
||||
|
||||
settings = frappe.get_doc("Non Profit Settings")
|
||||
@@ -225,16 +226,7 @@ def verify_signature(data, endpoint="Membership"):
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def trigger_razorpay_subscription(*args, **kwargs):
|
||||
data = frappe.request.get_data(as_text=True)
|
||||
try:
|
||||
verify_signature(data)
|
||||
except Exception as e:
|
||||
log = frappe.log_error(e, "Membership Webhook Verification Error")
|
||||
notify_failure(log)
|
||||
return { "status": "Failed", "reason": e}
|
||||
|
||||
if isinstance(data, six.string_types):
|
||||
data = json.loads(data)
|
||||
data = frappe._dict(data)
|
||||
data = process_request_data(data)
|
||||
|
||||
subscription = data.payload.get("subscription", {}).get("entity", {})
|
||||
subscription = frappe._dict(subscription)
|
||||
@@ -281,7 +273,7 @@ def trigger_razorpay_subscription(*args, **kwargs):
|
||||
# Update membership values
|
||||
member.subscription_start = datetime.fromtimestamp(subscription.start_at)
|
||||
member.subscription_end = datetime.fromtimestamp(subscription.end_at)
|
||||
member.subscription_activated = 1
|
||||
member.subscription_status = "Active"
|
||||
member.flags.ignore_mandatory = True
|
||||
member.save()
|
||||
|
||||
@@ -294,9 +286,67 @@ def trigger_razorpay_subscription(*args, **kwargs):
|
||||
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id)
|
||||
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
|
||||
notify_failure(log)
|
||||
return { "status": "Failed", "reason": e}
|
||||
return {"status": "Failed", "reason": e}
|
||||
|
||||
return { "status": "Success" }
|
||||
return {"status": "Success"}
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def update_halted_razorpay_subscription(*args, **kwargs):
|
||||
"""
|
||||
When all retries have been exhausted, Razorpay moves the subscription to the halted state.
|
||||
The customer has to manually retry the charge or change the card linked to the subscription,
|
||||
for the subscription to move back to the active state.
|
||||
"""
|
||||
if frappe.request:
|
||||
data = frappe.request.get_data(as_text=True)
|
||||
data = process_request_data(data)
|
||||
elif frappe.flags.in_test:
|
||||
data = kwargs.get("data")
|
||||
data = frappe._dict(data)
|
||||
else:
|
||||
return
|
||||
|
||||
if not data.event == "subscription.halted":
|
||||
return
|
||||
|
||||
subscription = data.payload.get("subscription", {}).get("entity", {})
|
||||
subscription = frappe._dict(subscription)
|
||||
|
||||
try:
|
||||
member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id)
|
||||
if not member:
|
||||
frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id))
|
||||
|
||||
member.subscription_status = "Halted"
|
||||
member.flags.ignore_mandatory = True
|
||||
member.save()
|
||||
|
||||
if subscription.get("notes"):
|
||||
member = get_additional_notes(member, subscription)
|
||||
|
||||
except Exception as e:
|
||||
message = "{0}\n\n{1}".format(e, frappe.get_traceback())
|
||||
log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name))
|
||||
notify_failure(log)
|
||||
return {"status": "Failed", "reason": e}
|
||||
|
||||
return {"status": "Success"}
|
||||
|
||||
|
||||
def process_request_data(data):
|
||||
try:
|
||||
verify_signature(data)
|
||||
except Exception as e:
|
||||
log = frappe.log_error(e, "Membership Webhook Verification Error")
|
||||
notify_failure(log)
|
||||
return {"status": "Failed", "reason": e}
|
||||
|
||||
if isinstance(data, six.string_types):
|
||||
data = json.loads(data)
|
||||
data = frappe._dict(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_company_for_memberships():
|
||||
@@ -362,4 +412,4 @@ def set_expired_status():
|
||||
`tabMembership` SET `status` = 'Expired'
|
||||
WHERE
|
||||
`status` not in ('Cancelled') AND `to_date` < %s
|
||||
""", (nowdate()))
|
||||
""", (nowdate()))
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
import frappe
|
||||
import erpnext
|
||||
from erpnext.non_profit.doctype.member.member import create_member
|
||||
from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription
|
||||
from frappe.utils import nowdate, add_months
|
||||
|
||||
class TestMembership(unittest.TestCase):
|
||||
@@ -13,11 +14,16 @@ class TestMembership(unittest.TestCase):
|
||||
plan = setup_membership()
|
||||
|
||||
# make test member
|
||||
self.member_doc = create_member(frappe._dict({
|
||||
'fullname': "_Test_Member",
|
||||
'email': "_test_member_erpnext@example.com",
|
||||
'plan_id': plan.name
|
||||
}))
|
||||
self.member_doc = create_member(
|
||||
frappe._dict({
|
||||
"fullname": "_Test_Member",
|
||||
"email": "_test_member_erpnext@example.com",
|
||||
"plan_id": plan.name,
|
||||
"subscription_id": "sub_DEX6xcJ1HSW4CR",
|
||||
"customer_id": "cust_C0WlbKhp3aLA7W",
|
||||
"subscription_status": "Active"
|
||||
})
|
||||
)
|
||||
self.member_doc.make_customer_and_link()
|
||||
self.member = self.member_doc.name
|
||||
|
||||
@@ -51,6 +57,20 @@ class TestMembership(unittest.TestCase):
|
||||
"to_date": add_months(nowdate(), 3),
|
||||
})
|
||||
|
||||
def test_halted_memberships(self):
|
||||
make_membership(self.member, {
|
||||
"from_date": add_months(nowdate(), 2),
|
||||
"to_date": add_months(nowdate(), 3)
|
||||
})
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active")
|
||||
payload = get_subscription_payload()
|
||||
update_halted_razorpay_subscription(data=payload)
|
||||
self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def set_config(key, value):
|
||||
frappe.db.set_value("Non Profit Settings", None, key, value)
|
||||
|
||||
@@ -115,4 +135,28 @@ def setup_membership():
|
||||
else:
|
||||
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
|
||||
|
||||
return plan
|
||||
return plan
|
||||
|
||||
def get_subscription_payload():
|
||||
return {
|
||||
"entity": "event",
|
||||
"account_id": "acc_BFQ7uQEaa7j2z7",
|
||||
"event": "subscription.halted",
|
||||
"contains": [
|
||||
"subscription"
|
||||
],
|
||||
"payload": {
|
||||
"subscription": {
|
||||
"entity": {
|
||||
"id": "sub_DEX6xcJ1HSW4CR",
|
||||
"entity": "subscription",
|
||||
"plan_id": "_rzpy_test_milythm",
|
||||
"customer_id": "cust_C0WlbKhp3aLA7W",
|
||||
"status": "halted",
|
||||
"notes": {
|
||||
"Important": "Notes for Internal Reference"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,3 +290,6 @@ erpnext.patches.v13_0.set_training_event_attendance
|
||||
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
|
||||
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
|
||||
erpnext.patches.v13_0.update_job_card_details
|
||||
erpnext.patches.v13_0.update_level_in_bom #1234sswef
|
||||
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
|
||||
erpnext.patches.v13_0.update_subscription_status_in_memberships
|
||||
|
||||
110
erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
Normal file
110
erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# Copyright (c) 2020, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cstr, flt, cint
|
||||
from erpnext.stock.stock_ledger import make_sl_entries
|
||||
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
|
||||
|
||||
def execute():
|
||||
if not frappe.db.has_column('Work Order', 'has_batch_no'):
|
||||
return
|
||||
|
||||
if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')):
|
||||
return
|
||||
|
||||
frappe.reload_doc('manufacturing', 'doctype', 'work_order')
|
||||
filters = {
|
||||
'docstatus': 1,
|
||||
'produced_qty': ('>', 0),
|
||||
'creation': ('>=', '2021-06-29 00:00:00'),
|
||||
'has_batch_no': 1
|
||||
}
|
||||
|
||||
fields = ['name', 'production_item']
|
||||
|
||||
work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)]
|
||||
|
||||
if not work_orders:
|
||||
return
|
||||
|
||||
repost_stock_entries = []
|
||||
stock_entries = frappe.db.sql_list('''
|
||||
SELECT
|
||||
se.name
|
||||
FROM
|
||||
`tabStock Entry` se
|
||||
WHERE
|
||||
se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders}
|
||||
and not exists(
|
||||
select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1
|
||||
)
|
||||
Order BY
|
||||
se.posting_date, se.posting_time
|
||||
'''.format(work_orders=tuple(work_orders)))
|
||||
|
||||
if stock_entries:
|
||||
print('Length of stock entries', len(stock_entries))
|
||||
|
||||
for stock_entry in stock_entries:
|
||||
doc = frappe.get_doc('Stock Entry', stock_entry)
|
||||
doc.set_work_order_details()
|
||||
doc.load_items_from_bom()
|
||||
doc.calculate_rate_and_amount()
|
||||
set_expense_account(doc)
|
||||
doc.make_batches('t_warehouse')
|
||||
|
||||
if doc.docstatus == 0:
|
||||
doc.save()
|
||||
else:
|
||||
repost_stock_entry(doc)
|
||||
repost_stock_entries.append(doc)
|
||||
|
||||
for repost_doc in repost_stock_entries:
|
||||
repost_future_sle_and_gle(repost_doc)
|
||||
|
||||
def set_expense_account(doc):
|
||||
for row in doc.items:
|
||||
if row.is_finished_item and not row.expense_account:
|
||||
row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account')
|
||||
|
||||
def repost_stock_entry(doc):
|
||||
doc.db_update()
|
||||
for child_row in doc.items:
|
||||
if child_row.is_finished_item:
|
||||
child_row.db_update()
|
||||
|
||||
sl_entries = []
|
||||
finished_item_row = doc.get_finished_item_row()
|
||||
get_sle_for_target_warehouse(doc, sl_entries, finished_item_row)
|
||||
|
||||
if sl_entries:
|
||||
try:
|
||||
make_sl_entries(sl_entries, True)
|
||||
except Exception:
|
||||
print(f'SLE entries not posted for the stock entry {doc.name}')
|
||||
traceback = frappe.get_traceback()
|
||||
frappe.log_error(traceback)
|
||||
|
||||
def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row):
|
||||
for d in doc.get('items'):
|
||||
if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name:
|
||||
sle = doc.get_sl_entries(d, {
|
||||
"warehouse": cstr(d.t_warehouse),
|
||||
"actual_qty": flt(d.transfer_qty),
|
||||
"incoming_rate": flt(d.valuation_rate)
|
||||
})
|
||||
|
||||
sle.recalculate_rate = 1
|
||||
sl_entries.append(sle)
|
||||
|
||||
def repost_future_sle_and_gle(doc):
|
||||
args = frappe._dict({
|
||||
"posting_date": doc.posting_date,
|
||||
"posting_time": doc.posting_time,
|
||||
"voucher_type": doc.doctype,
|
||||
"voucher_no": doc.name,
|
||||
"company": doc.company
|
||||
})
|
||||
|
||||
create_repost_item_valuation_entry(args)
|
||||
30
erpnext/patches/v13_0/update_level_in_bom.py
Normal file
30
erpnext/patches/v13_0/update_level_in_bom.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2020, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
for document in ["bom", "bom_item", "bom_explosion_item"]:
|
||||
frappe.reload_doc('manufacturing', 'doctype', document)
|
||||
|
||||
frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1")
|
||||
|
||||
bom_list = frappe.db.sql_list("""select name from `tabBOM` bom
|
||||
where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item`
|
||||
where parent=bom.name and ifnull(bom_no, '')!='')""")
|
||||
|
||||
count = 0
|
||||
while(count < len(bom_list)):
|
||||
for parent_bom in get_parent_boms(bom_list[count]):
|
||||
bom_doc = frappe.get_cached_doc("BOM", parent_bom)
|
||||
bom_doc.set_bom_level(update=True)
|
||||
bom_list.append(parent_bom)
|
||||
count += 1
|
||||
|
||||
def get_parent_boms(bom_no):
|
||||
return frappe.db.sql_list("""
|
||||
select distinct bom_item.parent from `tabBOM Item` bom_item
|
||||
where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
|
||||
and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
|
||||
""", bom_no)
|
||||
@@ -0,0 +1,9 @@
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
if frappe.db.exists('DocType', 'Member'):
|
||||
frappe.reload_doc('Non Profit', 'doctype', 'Member')
|
||||
|
||||
if frappe.db.has_column('Member', 'subscription_activated'):
|
||||
frappe.db.sql('UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1')
|
||||
frappe.db.sql_ddl('ALTER table `tabMember` DROP COLUMN subscription_activated')
|
||||
@@ -110,11 +110,11 @@ class AdditionalSalary(Document):
|
||||
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
|
||||
return amount_per_day * no_of_days
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_additional_salaries(employee, start_date, end_date, component_type):
|
||||
additional_salary_list = frappe.db.sql("""
|
||||
select name, salary_component as component, type, amount,
|
||||
overwrite_salary_structure_amount as overwrite,
|
||||
deduct_full_tax_on_selected_payroll_date
|
||||
select name, salary_component as component, type, amount, overwrite_salary_structure_amount as overwrite,
|
||||
deduct_full_tax_on_selected_payroll_date, is_recurring
|
||||
from `tabAdditional Salary`
|
||||
where employee=%(employee)s
|
||||
and docstatus = 1
|
||||
|
||||
@@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
});
|
||||
|
||||
frm.set_query('employee', 'employees', () => {
|
||||
if (!frm.doc.company) {
|
||||
frappe.msgprint(__("Please set a Company"));
|
||||
return [];
|
||||
let error_fields = [];
|
||||
let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date'];
|
||||
|
||||
let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]);
|
||||
|
||||
mandatory_fields.forEach(field => {
|
||||
if (!frm.doc[field]) {
|
||||
error_fields.push(frappe.unscrub(field));
|
||||
}
|
||||
});
|
||||
|
||||
if (error_fields && error_fields.length) {
|
||||
message = message + '<br><br><ul><li>' + error_fields.join('</li><li>') + "</ul>";
|
||||
frappe.throw({
|
||||
message: message,
|
||||
indicator: 'red',
|
||||
title: __('Missing Fields')
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query",
|
||||
filters: frm.events.get_employee_filters(frm)
|
||||
@@ -148,25 +164,22 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
|
||||
get_employee_filters: function (frm) {
|
||||
let filters = {};
|
||||
filters['company'] = frm.doc.company;
|
||||
filters['start_date'] = frm.doc.start_date;
|
||||
filters['end_date'] = frm.doc.end_date;
|
||||
filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet;
|
||||
filters['payroll_frequency'] = frm.doc.payroll_frequency;
|
||||
filters['payroll_payable_account'] = frm.doc.payroll_payable_account;
|
||||
filters['currency'] = frm.doc.currency;
|
||||
|
||||
if (frm.doc.department) {
|
||||
filters['department'] = frm.doc.department;
|
||||
}
|
||||
if (frm.doc.branch) {
|
||||
filters['branch'] = frm.doc.branch;
|
||||
}
|
||||
if (frm.doc.designation) {
|
||||
filters['designation'] = frm.doc.designation;
|
||||
}
|
||||
let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account',
|
||||
'currency', 'department', 'branch', 'designation'];
|
||||
|
||||
fields.forEach(field => {
|
||||
if (frm.doc[field]) {
|
||||
filters[field] = frm.doc[field];
|
||||
}
|
||||
});
|
||||
|
||||
if (frm.doc.employees) {
|
||||
filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
|
||||
let employees = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
|
||||
if (employees && employees.length) {
|
||||
filters['employees'] = employees;
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
},
|
||||
|
||||
@@ -117,7 +117,6 @@ class PayrollEntry(Document):
|
||||
Creates salary slip for selected employees if already not created
|
||||
"""
|
||||
self.check_permission('write')
|
||||
self.created = 1
|
||||
employees = [emp.employee for emp in self.employees]
|
||||
if employees:
|
||||
args = frappe._dict({
|
||||
@@ -459,6 +458,7 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account):
|
||||
where
|
||||
t1.name = t2.employee
|
||||
and t2.docstatus = 1
|
||||
and t1.status != 'Inactive'
|
||||
%s order by t2.from_date desc
|
||||
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
|
||||
|
||||
@@ -679,9 +679,13 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
conditions = []
|
||||
include_employees = []
|
||||
emp_cond = ''
|
||||
|
||||
if not filters.payroll_frequency:
|
||||
frappe.throw(_('Select Payroll Frequency.'))
|
||||
|
||||
if filters.start_date and filters.end_date:
|
||||
employee_list = get_employee_list(filters)
|
||||
emp = filters.get('employees')
|
||||
emp = filters.get('employees') or []
|
||||
include_employees = [employee.employee for employee in employee_list if employee.employee not in emp]
|
||||
filters.pop('start_date')
|
||||
filters.pop('end_date')
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"year_to_date",
|
||||
"section_break_5",
|
||||
"additional_salary",
|
||||
"is_recurring_additional_salary",
|
||||
"statistical_component",
|
||||
"depends_on_payment_days",
|
||||
"exempted_from_income_tax",
|
||||
@@ -235,11 +236,19 @@
|
||||
"label": "Year To Date",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='earnings' && doc.additional_salary",
|
||||
"fieldname": "is_recurring_additional_salary",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Recurring Additional Salary",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-14 13:39:15.847158",
|
||||
"modified": "2021-03-14 13:39:15.847158",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Salary Detail",
|
||||
|
||||
@@ -7,12 +7,12 @@ import datetime, math
|
||||
|
||||
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
from frappe import msgprint, _
|
||||
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
|
||||
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
|
||||
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
|
||||
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
|
||||
@@ -616,7 +616,8 @@ class SalarySlip(TransactionBase):
|
||||
get_salary_component_data(additional_salary.component),
|
||||
additional_salary.amount,
|
||||
component_type,
|
||||
additional_salary
|
||||
additional_salary,
|
||||
is_recurring = additional_salary.is_recurring
|
||||
)
|
||||
|
||||
def add_tax_components(self, payroll_period):
|
||||
@@ -637,7 +638,7 @@ class SalarySlip(TransactionBase):
|
||||
tax_row = get_salary_component_data(d)
|
||||
self.update_component_row(tax_row, tax_amount, "deductions")
|
||||
|
||||
def update_component_row(self, component_data, amount, component_type, additional_salary=None):
|
||||
def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0):
|
||||
component_row = None
|
||||
for d in self.get(component_type):
|
||||
if d.salary_component != component_data.salary_component:
|
||||
@@ -678,6 +679,7 @@ class SalarySlip(TransactionBase):
|
||||
component_row.set('abbr', abbr)
|
||||
|
||||
if additional_salary:
|
||||
component_row.is_recurring_additional_salary = is_recurring
|
||||
component_row.default_amount = 0
|
||||
component_row.additional_amount = amount
|
||||
component_row.additional_salary = additional_salary.name
|
||||
@@ -711,6 +713,7 @@ class SalarySlip(TransactionBase):
|
||||
# get remaining numbers of sub-period (period for which one salary is processed)
|
||||
remaining_sub_periods = get_period_factor(self.employee,
|
||||
self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1]
|
||||
|
||||
# get taxable_earnings, paid_taxes for previous period
|
||||
previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date,
|
||||
self.start_date, tax_slab.allow_tax_exemption)
|
||||
@@ -870,8 +873,16 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
if earning.is_tax_applicable:
|
||||
if additional_amount:
|
||||
taxable_earnings += (amount - additional_amount)
|
||||
additional_income += additional_amount
|
||||
if not earning.is_recurring_additional_salary:
|
||||
taxable_earnings += (amount - additional_amount)
|
||||
additional_income += additional_amount
|
||||
else:
|
||||
to_date = frappe.db.get_value("Additional Salary", earning.additional_salary, 'to_date')
|
||||
period = (getdate(to_date).month - getdate(self.start_date).month) + 1
|
||||
if period > 0:
|
||||
taxable_earnings += (amount - additional_amount) * period
|
||||
additional_income += additional_amount * period
|
||||
|
||||
if earning.deduct_full_tax_on_selected_payroll_date:
|
||||
additional_income_with_full_tax += additional_amount
|
||||
continue
|
||||
@@ -1091,6 +1102,7 @@ class SalarySlip(TransactionBase):
|
||||
"applicant": self.employee,
|
||||
"docstatus": 1,
|
||||
"repay_from_salary": 1,
|
||||
"company": self.company
|
||||
})
|
||||
|
||||
def make_loan_repayment_entry(self):
|
||||
|
||||
@@ -482,14 +482,19 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
|
||||
|
||||
|
||||
employee = frappe.db.get_value("Employee", {"user_id": user})
|
||||
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
|
||||
employee = frappe.db.get_value("Employee",
|
||||
{
|
||||
"user_id": user
|
||||
},
|
||||
["name", "company", "employee_name"],
|
||||
as_dict=True)
|
||||
|
||||
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company)
|
||||
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
|
||||
|
||||
if not salary_slip_name:
|
||||
salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee)
|
||||
salary_slip.employee_name = frappe.get_value("Employee",
|
||||
{"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name")
|
||||
salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee.name)
|
||||
salary_slip.employee_name = employee.employee_name
|
||||
salary_slip.payroll_frequency = payroll_frequency
|
||||
salary_slip.posting_date = nowdate()
|
||||
salary_slip.insert()
|
||||
|
||||
@@ -119,26 +119,25 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None,
|
||||
if test_tax:
|
||||
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
|
||||
|
||||
if not frappe.db.exists('Salary Structure', salary_structure):
|
||||
details = {
|
||||
"doctype": "Salary Structure",
|
||||
"name": salary_structure,
|
||||
"company": company or erpnext.get_default_company(),
|
||||
"earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"payroll_frequency": payroll_frequency,
|
||||
"payment_account": get_random("Account", filters={'account_currency': currency}),
|
||||
"currency": currency
|
||||
}
|
||||
if other_details and isinstance(other_details, dict):
|
||||
details.update(other_details)
|
||||
salary_structure_doc = frappe.get_doc(details)
|
||||
salary_structure_doc.insert()
|
||||
if not dont_submit:
|
||||
salary_structure_doc.submit()
|
||||
if frappe.db.exists("Salary Structure", salary_structure):
|
||||
frappe.db.delete("Salary Structure", salary_structure)
|
||||
|
||||
else:
|
||||
salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure)
|
||||
details = {
|
||||
"doctype": "Salary Structure",
|
||||
"name": salary_structure,
|
||||
"company": company or erpnext.get_default_company(),
|
||||
"earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"payroll_frequency": payroll_frequency,
|
||||
"payment_account": get_random("Account", filters={'account_currency': currency}),
|
||||
"currency": currency
|
||||
}
|
||||
if other_details and isinstance(other_details, dict):
|
||||
details.update(other_details)
|
||||
salary_structure_doc = frappe.get_doc(details)
|
||||
salary_structure_doc.insert()
|
||||
if not dont_submit:
|
||||
salary_structure_doc.submit()
|
||||
|
||||
filters = {'employee':employee, 'docstatus': 1}
|
||||
if not from_date and payroll_period:
|
||||
|
||||
@@ -2,6 +2,7 @@ import frappe
|
||||
from frappe.utils import cint
|
||||
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
|
||||
from erpnext.shopping_cart.product_info import get_product_info_for_website
|
||||
from erpnext.setup.doctype.item_group.item_group import get_child_groups
|
||||
|
||||
def get_field_filter_data():
|
||||
product_settings = get_product_settings()
|
||||
@@ -89,6 +90,7 @@ def get_products_for_website(field_filters=None, attribute_filters=None, search=
|
||||
def get_products_html_for_website(field_filters=None, attribute_filters=None):
|
||||
field_filters = frappe.parse_json(field_filters)
|
||||
attribute_filters = frappe.parse_json(attribute_filters)
|
||||
set_item_group_filters(field_filters)
|
||||
|
||||
items = get_products_for_website(field_filters, attribute_filters)
|
||||
html = ''.join(get_html_for_items(items))
|
||||
@@ -98,6 +100,10 @@ def get_products_html_for_website(field_filters=None, attribute_filters=None):
|
||||
|
||||
return html
|
||||
|
||||
def set_item_group_filters(field_filters):
|
||||
if field_filters is not None and 'item_group' in field_filters:
|
||||
field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])]
|
||||
|
||||
|
||||
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
|
||||
items = []
|
||||
|
||||
@@ -67,6 +67,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
|
||||
calculate_discount_amount: function(){
|
||||
if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) {
|
||||
this.calculate_item_values();
|
||||
this.calculate_net_total();
|
||||
this.set_discount_amount();
|
||||
this.apply_discount_amount();
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ frappe.help.help_links["permission-manager"] = [
|
||||
|
||||
frappe.help.help_links["Form/System Settings"] = [
|
||||
{
|
||||
label: "Naming Series",
|
||||
label: "System Settings",
|
||||
url: docsUrl + "user/manual/en/setting-up/settings/system-settings",
|
||||
},
|
||||
];
|
||||
@@ -206,7 +206,7 @@ frappe.help.help_links["Form/PayPal Settings"] = [
|
||||
label: "PayPal Settings",
|
||||
url:
|
||||
docsUrl +
|
||||
"user/manual/en/setting-up/integrations/paypal-integration",
|
||||
"user/manual/en/erpnext_integration/paypal-integration",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -215,14 +215,14 @@ frappe.help.help_links["Form/Razorpay Settings"] = [
|
||||
label: "Razorpay Settings",
|
||||
url:
|
||||
docsUrl +
|
||||
"user/manual/en/setting-up/integrations/razorpay-integration",
|
||||
"user/manual/en/erpnext_integration/razorpay-integration",
|
||||
},
|
||||
];
|
||||
|
||||
frappe.help.help_links["Form/Dropbox Settings"] = [
|
||||
{
|
||||
label: "Dropbox Settings",
|
||||
url: docsUrl + "user/manual/en/setting-up/integrations/dropbox-backup",
|
||||
url: docsUrl + "user/manual/en/erpnext_integration/dropbox-backup",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -230,7 +230,7 @@ frappe.help.help_links["Form/LDAP Settings"] = [
|
||||
{
|
||||
label: "LDAP Settings",
|
||||
url:
|
||||
docsUrl + "user/manual/en/setting-up/integrations/ldap-integration",
|
||||
docsUrl + "user/manual/en/erpnext_integration/ldap-integration",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -239,7 +239,7 @@ frappe.help.help_links["Form/Stripe Settings"] = [
|
||||
label: "Stripe Settings",
|
||||
url:
|
||||
docsUrl +
|
||||
"user/manual/en/setting-up/integrations/stripe-integration",
|
||||
"user/manual/en/erpnext_integration/stripe-integration",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -991,7 +991,7 @@ frappe.help.help_links["Form/BOM"] = [
|
||||
label: "Nested BOM Structure",
|
||||
url:
|
||||
docsUrl +
|
||||
"user/manual/en/manufacturing/articles/nested-bom-structure",
|
||||
"user/manual/en/manufacturing/articles/managing-multi-level-bom",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ erpnext.setup.slides_settings = [
|
||||
}
|
||||
|
||||
// Validate bank name
|
||||
if(me.values.bank_account){
|
||||
if(me.values.bank_account) {
|
||||
frappe.call({
|
||||
async: false,
|
||||
method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
frappe.provide('frappe.ui.form');
|
||||
|
||||
frappe.ui.form.CustomerQuickEntryForm = frappe.ui.form.QuickEntryForm.extend({
|
||||
init: function(doctype, after_insert) {
|
||||
init: function(doctype, after_insert, init_callback, doc, force) {
|
||||
this._super(doctype, after_insert, init_callback, doc, force);
|
||||
this.skip_redirect_on_error = true;
|
||||
this._super(doctype, after_insert);
|
||||
},
|
||||
|
||||
render_dialog: function() {
|
||||
|
||||
@@ -35,6 +35,7 @@ frappe.ui.form.on('GST Settings', {
|
||||
return {
|
||||
filters: {
|
||||
company: row.company,
|
||||
account_type: "Tax",
|
||||
is_group: 0
|
||||
}
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user