Merge branch 'version-13-hotfix' into feat-bom-process-loss

This commit is contained in:
Alan
2021-07-21 17:13:36 +05:30
committed by GitHub
153 changed files with 4590 additions and 2589 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -953,6 +953,120 @@ class TestPurchaseInvoice(unittest.TestCase):
acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save() acc_settings.save()
def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value(
"Accounts Settings", "Accounts Settings",
"unlink_payment_on_cancel_of_invoice")
frappe.db.set_value(
"Accounts Settings", "Accounts Settings",
"unlink_payment_on_cancel_of_invoice", 1)
original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account")
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC")
pay = frappe.get_doc({
'doctype': 'Payment Entry',
'company': '_Test Company',
'payment_type': 'Pay',
'party_type': 'Supplier',
'party': '_Test Supplier USD',
'paid_to': '_Test Payable USD - _TC',
'paid_from': 'Cash - _TC',
'paid_amount': 70000,
'target_exchange_rate': 70,
'received_amount': 1000,
})
pay.insert()
pay.submit()
pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD",
conversion_rate=75, rate=500, do_not_save=1, qty=1)
pi.cost_center = "_Test Cost Center - _TC"
pi.advances = []
pi.append("advances", {
"reference_type": "Payment Entry",
"reference_name": pay.name,
"advance_amount": 1000,
"remarks": pay.remarks,
"allocated_amount": 500,
"ref_exchange_rate": 70
})
pi.save()
pi.submit()
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 37500.0],
["_Test Payable USD - _TC", -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): def test_purchase_invoice_advance_taxes(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -1010,21 +1124,21 @@ class TestPurchaseInvoice(unittest.TestCase):
# Check GLE for Purchase Invoice # Check GLE for Purchase Invoice
# Zero net effect on final TDS Payable on invoice # Zero net effect on final TDS Payable on invoice
expected_gle = [ expected_gle = [
['_Test Account Cost for Goods Sold - _TC', 30000, 0], ['_Test Account Cost for Goods Sold - _TC', 30000],
['_Test Account Excise Duty - _TC', 0, 3000], ['_Test Account Excise Duty - _TC', -3000],
['Creditors - _TC', 0, 27000], ['Creditors - _TC', -27000],
['TDS Payable - _TC', 3000, 3000] ['TDS Payable - _TC', 0]
] ]
gl_entries = frappe.db.sql("""select account, debit, credit gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
from `tabGL Entry` from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s where voucher_type='Purchase Invoice' and voucher_no=%s
group by account
order by account asc""", (purchase_invoice.name), as_dict=1) order by account asc""", (purchase_invoice.name), as_dict=1)
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.debit) self.assertEqual(expected_gle[i][1], gle.amount)
self.assertEqual(expected_gle[i][2], gle.credit)
def update_tax_witholding_category(company, account, date): def update_tax_witholding_category(company, account, date):
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -472,7 +472,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
"total_amount": d.grand_total, "total_amount": d.grand_total,
"outstanding_amount": d.outstanding_amount, "outstanding_amount": d.outstanding_amount,
"allocated_amount": d.allocated_amount, "allocated_amount": d.allocated_amount,
"exchange_rate": d.exchange_rate "exchange_rate": d.exchange_rate if not d.exchange_gain_loss else payment_entry.get_exchange_rate(),
"exchange_gain_loss": d.exchange_gain_loss # only populated from invoice in case of advance allocation
} }
if d.voucher_detail_no: if d.voucher_detail_no:
@@ -498,12 +499,15 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
payment_entry.set_amounts() payment_entry.set_amounts()
if d.difference_amount and d.difference_account: if d.difference_amount and d.difference_account:
payment_entry.set_gain_or_loss(account_details={ account_details = {
'account': d.difference_account, 'account': d.difference_account,
'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company', 'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company',
payment_entry.company, "cost_center"), payment_entry.company, "cost_center")
'amount': d.difference_amount }
}) if d.difference_amount:
account_details['amount'] = d.difference_amount
payment_entry.set_gain_or_loss(account_details=account_details)
if not do_not_save: if not do_not_save:
payment_entry.save(ignore_permissions=True) payment_entry.save(ignore_permissions=True)
@@ -784,7 +788,7 @@ def get_children(doctype, parent, company, is_root=False):
return acc return acc
def create_payment_gateway_account(gateway, payment_channel="Email"): def create_payment_gateway_account(gateway, payment_channel="Email"):
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
company = frappe.db.get_value("Global Defaults", None, "default_company") company = frappe.db.get_value("Global Defaults", None, "default_company")
if not company: if not company:

View File

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

View File

@@ -124,6 +124,8 @@ class AccountsController(TransactionBase):
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
self.set_advances() self.set_advances()
self.set_advance_gain_or_loss()
if self.is_return: if self.is_return:
self.validate_qty() self.validate_qty()
else: else:
@@ -584,15 +586,18 @@ class AccountsController(TransactionBase):
allocated_amount = min(amount - advance_allocated, d.amount) allocated_amount = min(amount - advance_allocated, d.amount)
advance_allocated += flt(allocated_amount) advance_allocated += flt(allocated_amount)
self.append("advances", { advance_row = {
"doctype": self.doctype + " Advance", "doctype": self.doctype + " Advance",
"reference_type": d.reference_type, "reference_type": d.reference_type,
"reference_name": d.reference_name, "reference_name": d.reference_name,
"reference_row": d.reference_row, "reference_row": d.reference_row,
"remarks": d.remarks, "remarks": d.remarks,
"advance_amount": flt(d.amount), "advance_amount": flt(d.amount),
"allocated_amount": allocated_amount "allocated_amount": allocated_amount,
}) "ref_exchange_rate": flt(d.exchange_rate) # exchange_rate of advance entry
}
self.append("advances", advance_row)
def get_advance_entries(self, include_unallocated=True): def get_advance_entries(self, include_unallocated=True):
if self.doctype == "Sales Invoice": if self.doctype == "Sales Invoice":
@@ -650,6 +655,66 @@ class AccountsController(TransactionBase):
"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.") "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.")
.format(d.reference_name, d.against_order)) .format(d.reference_name, d.against_order))
def set_advance_gain_or_loss(self):
if not self.get("advances"):
return
for d in self.get("advances"):
advance_exchange_rate = d.ref_exchange_rate
if (d.allocated_amount and self.conversion_rate != 1
and self.conversion_rate != advance_exchange_rate):
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate
d.exchange_gain_loss = difference
def make_exchange_gain_loss_gl_entries(self, gl_entries):
if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']:
for d in self.get("advances"):
if d.exchange_gain_loss:
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): def update_against_document_in_jv(self):
""" """
Links invoice and advance voucher: Links invoice and advance voucher:
@@ -690,7 +755,9 @@ class AccountsController(TransactionBase):
if self.party_account_currency != self.company_currency else 1), if self.party_account_currency != self.company_currency else 1),
'grand_total': (self.base_grand_total 'grand_total': (self.base_grand_total
if self.party_account_currency == self.company_currency else self.grand_total), if self.party_account_currency == self.company_currency else self.grand_total),
'outstanding_amount': self.outstanding_amount 'outstanding_amount': self.outstanding_amount,
'difference_account': frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account'),
'exchange_gain_loss': flt(d.get('exchange_gain_loss'))
}) })
lst.append(args) lst.append(args)
@@ -751,11 +818,11 @@ class AccountsController(TransactionBase):
account_currency = get_account_currency(tax.account_head) account_currency = get_account_currency(tax.account_head)
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
else:
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
else:
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
party = self.supplier if self.doctype == "Purchase Invoice" else self.customer party = self.supplier if self.doctype == "Purchase Invoice" else self.customer
unallocated_amount = tax.tax_amount - tax.allocated_amount unallocated_amount = tax.tax_amount - tax.allocated_amount
@@ -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" party_account_field = "paid_from" if party_type == "Customer" else "paid_to"
currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency"
payment_type = "Receive" if party_type == "Customer" else "Pay" payment_type = "Receive" if party_type == "Customer" else "Pay"
exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate"
payment_entries_against_order, unallocated_payment_entries = [], [] payment_entries_against_order, unallocated_payment_entries = [], []
limit_cond = "limit %s" % limit if limit else "" limit_cond = "limit %s" % limit if limit else ""
@@ -1305,27 +1374,28 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
"Payment Entry" as reference_type, t1.name as reference_name, "Payment Entry" as reference_type, t1.name as reference_name,
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
t2.reference_name as against_order, t1.posting_date, t2.reference_name as against_order, t1.posting_date,
t1.{0} as currency t1.{0} as currency, t1.{4} as exchange_rate
from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 from `tabPayment Entry` t1, `tabPayment Entry Reference` t2
where where
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
and t2.reference_doctype = %s {2} and t2.reference_doctype = %s {2}
order by t1.posting_date {3} order by t1.posting_date {3}
""".format(currency_field, party_account_field, reference_condition, limit_cond), """.format(currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field),
[party_account, payment_type, party_type, party, [party_account, payment_type, party_type, party,
order_doctype] + order_list, as_dict=1) order_doctype] + order_list, as_dict=1)
if include_unallocated: if include_unallocated:
unallocated_payment_entries = frappe.db.sql(""" unallocated_payment_entries = frappe.db.sql("""
select "Payment Entry" as reference_type, name as reference_name, select "Payment Entry" as reference_type, name as reference_name,
remarks, unallocated_amount as amount remarks, unallocated_amount as amount, {2} as exchange_rate
from `tabPayment Entry` from `tabPayment Entry`
where where
{0} = %s and party_type = %s and party = %s and payment_type = %s {0} = %s and party_type = %s and party = %s and payment_type = %s
and docstatus = 1 and unallocated_amount > 0 and docstatus = 1 and unallocated_amount > 0
order by posting_date {1} order by posting_date {1}
""".format(party_account_field, limit_cond), (party_account, party_type, party, payment_type), as_dict=1) """.format(party_account_field, limit_cond, exchange_rate_field),
(party_account, party_type, party, payment_type), as_dict=1)
return list(payment_entries_against_order) + list(unallocated_payment_entries) return list(payment_entries_against_order) + list(unallocated_payment_entries)

View File

@@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
import erpnext import erpnext
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map 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.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
@@ -356,42 +356,68 @@ class StockController(AccountsController):
}, update_modified) }, update_modified)
def validate_inspection(self): def validate_inspection(self):
'''Checks if quality inspection is set for Items that require inspection. """Checks if quality inspection is set/ is valid for Items that require inspection."""
On submit, throw an exception''' inspection_fieldname_map = {
inspection_required_fieldname = None "Purchase Receipt": "inspection_required_before_purchase",
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: "Purchase Invoice": "inspection_required_before_purchase",
inspection_required_fieldname = "inspection_required_before_purchase" "Sales Invoice": "inspection_required_before_delivery",
elif self.doctype in ["Delivery Note", "Sales Invoice"]: "Delivery Note": "inspection_required_before_delivery"
inspection_required_fieldname = "inspection_required_before_delivery" }
inspection_required_fieldname = inspection_fieldname_map.get(self.doctype)
# return if inspection is not required on document level
if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or
(self.doctype == "Stock Entry" and not self.inspection_required) or (self.doctype == "Stock Entry" and not self.inspection_required) or
(self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)): (self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)):
return return
for d in self.get('items'): for row in self.get('items'):
qa_required = False qi_required = False
if (inspection_required_fieldname and not d.quality_inspection and if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)):
frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)): qi_required = True
qa_required = True elif self.doctype == "Stock Entry" and row.t_warehouse:
elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse: qi_required = True # inward stock needs inspection
qa_required = True
if self.docstatus == 1 and d.quality_inspection:
qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection)
if qa_doc.docstatus == 0:
link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
if qa_doc.status != 'Accepted': if qi_required: # validate row only if inspection is required on item level
frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") self.validate_qi_presence(row)
.format(d.idx, d.item_code), QualityInspectionRejectedError) if self.docstatus == 1:
elif qa_required : self.validate_qi_submission(row)
action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted self.validate_qi_rejection(row)
if self.docstatus==1 and action == 'Stop':
frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)), def validate_qi_presence(self, row):
exc=QualityInspectionRequiredError) """Check if QI is present on row level. Warn on save and stop on submit if missing."""
else: if not row.quality_inspection:
frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code))) msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
if self.docstatus == 1:
frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
else:
frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
def validate_qi_submission(self, row):
"""Check if QI is submitted on row level, during submission"""
action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus")
if not qa_docstatus == 1:
link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
if action == "Stop":
frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
else:
frappe.msgprint(_(msg), alert=True, indicator="orange")
def validate_qi_rejection(self, row):
"""Check if QI is rejected on row level, during submission"""
action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected")
qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status")
if qa_status == "Rejected":
link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
if action == "Stop":
frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
else:
frappe.msgprint(_(msg), alert=True, indicator="orange")
def update_blanket_order(self): def update_blanket_order(self):
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))
@@ -497,9 +523,6 @@ class StockController(AccountsController):
}) })
if future_sle_exists(args): if future_sle_exists(args):
create_repost_item_valuation_entry(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() @frappe.whitelist()
def make_quality_inspections(doctype, docname, items): def make_quality_inspections(doctype, docname, items):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) {
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) { cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{ return{
query: "erpnext.controllers.queries.employee_query" query: "erpnext.controllers.queries.employee_query"
} }
} }

View File

@@ -15,6 +15,7 @@ class Attendance(Document):
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
self.validate_attendance_date() self.validate_attendance_date()
self.validate_duplicate_record() self.validate_duplicate_record()
self.validate_employee_status()
self.check_leave_record() self.check_leave_record()
def validate_attendance_date(self): 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.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
frappe.bold(self.employee), frappe.bold(self.attendance_date))) 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): def check_leave_record(self):
leave_record = frappe.db.sql(""" leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date select leave_type, half_day, half_day_date

View File

@@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = {
label: __('For Employee'), label: __('For Employee'),
fieldtype: 'Link', fieldtype: 'Link',
options: 'Employee', options: 'Employee',
get_query: () => {
return {query: "erpnext.controllers.queries.employee_query"}
},
reqd: 1, reqd: 1,
onchange: function() { onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1); dialog.set_df_property("unmarked_days", "hidden", 1);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -325,8 +325,7 @@ frappe.ui.form.on("BOM", {
freeze: true, freeze: true,
args: { args: {
update_parent: true, update_parent: true,
from_child_bom:false, from_child_bom:false
save: frm.doc.docstatus === 1 ? true : false
}, },
callback: function(r) { callback: function(r) {
refresh_field("items"); refresh_field("items");

View File

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

View File

@@ -1,7 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from typing import List
from collections import deque
import frappe, erpnext import frappe, erpnext
from frappe.utils import cint, cstr, flt, today from frappe.utils import cint, cstr, flt, today
from frappe import _ from frappe import _
@@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc
import functools import functools
from six import string_types
from operator import itemgetter from operator import itemgetter
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "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): class BOM(WebsiteGenerator):
website = frappe._dict( website = frappe._dict(
# page_title_field = "item_name", # page_title_field = "item_name",
@@ -83,6 +155,7 @@ class BOM(WebsiteGenerator):
self.update_stock_qty() self.update_stock_qty()
self.validate_scrap_items() self.validate_scrap_items()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
self.set_bom_level()
def get_context(self, context): def get_context(self, context):
@@ -154,7 +227,7 @@ class BOM(WebsiteGenerator):
if not args: if not args:
args = frappe.form_dict.get('args') args = frappe.form_dict.get('args')
if isinstance(args, string_types): if isinstance(args, str):
import json import json
args = json.loads(args) args = json.loads(args)
@@ -602,6 +675,7 @@ class BOM(WebsiteGenerator):
if not d.batch_size or d.batch_size <= 0: if not d.batch_size or d.batch_size <= 0:
d.batch_size = 1 d.batch_size = 1
def validate_scrap_items(self): def validate_scrap_items(self):
for item in self.scrap_items: for item in self.scrap_items:
if item.item_code == self.item and not item.is_process_loss: if item.item_code == self.item and not item.is_process_loss:
@@ -628,6 +702,24 @@ class BOM(WebsiteGenerator):
' ' + _('set to 0 because') + ' ' + ' ' + _('set to 0 because') + ' ' +
frappe.bold(_('Is Process Loss')) + ' ' + _('is checked')) 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): def get_bom_item_rate(args, bom_doc):
if bom_doc.rm_cost_as_per == 'Valuation Rate': if bom_doc.rm_cost_as_per == 'Valuation Rate':
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) 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_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function
"conversion_factor": args.get("conversion_factor") or 1, "conversion_factor": args.get("conversion_factor") or 1,
"plc_conversion_rate": 1, "plc_conversion_rate": 1,
"ignore_party": True "ignore_party": True,
"ignore_conversion_rate": True
}) })
item_doc = frappe.get_cached_doc("Item", args.get("item_code")) item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
out = frappe._dict() out = frappe._dict()
@@ -814,7 +907,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
frappe.form_dict.parent = parent frappe.form_dict.parent = parent
if frappe.form_dict.parent: if frappe.form_dict.parent:
bom_doc = frappe.get_doc("BOM", frappe.form_dict.parent) bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent)
frappe.has_permission("BOM", doc=bom_doc, throw=True) frappe.has_permission("BOM", doc=bom_doc, throw=True)
bom_items = frappe.get_all('BOM Item', bom_items = frappe.get_all('BOM Item',
@@ -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) item_names = tuple(d.get('item_code') for d in bom_items)
items = frappe.get_list('Item', items = frappe.get_list('Item',
fields=['image', 'description', 'name', 'stock_uom', 'item_name'], fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'],
filters=[['name', 'in', item_names]]) # to get only required item dicts filters=[['name', 'in', item_names]]) # to get only required item dicts
for bom_item in bom_items: for bom_item in bom_items:
@@ -838,6 +931,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
bom_item.parent_bom_qty = bom_doc.quantity bom_item.parent_bom_qty = bom_doc.quantity
bom_item.expandable = 0 if bom_item.value in ('', None) else 1 bom_item.expandable = 0 if bom_item.value in ('', None) else 1
bom_item.image = frappe.db.escape(bom_item.image)
return bom_items return bom_items
@@ -1054,6 +1148,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
}, },
'BOM Item': { 'BOM Item': {
'doctype': 'BOM Item', 'doctype': 'BOM Item',
# stop get_mapped_doc copying parent bom_no to children
'field_no_map': ['bom_no'],
'condition': lambda doc: doc.has_variants == 0 'condition': lambda doc: doc.has_variants == 0
}, },
}, target_doc, postprocess) }, target_doc, postprocess)

View File

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

View File

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

View File

@@ -2,14 +2,13 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from collections import deque
import unittest import unittest
import frappe import frappe
from frappe.utils import cstr, flt from frappe.utils import cstr, flt
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost 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.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.tests.test_subcontracting import set_backflush_based_on 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]) supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
self.assertEqual(bom_items, supplied_items) self.assertEqual(bom_items, supplied_items)
def test_bom_with_process_loss_item(self): def test_bom_with_process_loss_item(self):
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() 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 # FG Items in Scrap/Loss Table should have Is Process Loss set
self.assertRaises(frappe.ValidationError, bom_doc.submit) 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"): 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}) 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): 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] warehouse_list = [warehouse_list]
if not warehouse_list: if not warehouse_list:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase):
ste.submit() ste.submit()
stock_entries.append(ste) 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)) self.assertEqual(len(job_cards), len(bom.operations))
for i, job_card in enumerate(job_cards): for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card) doc = frappe.get_doc("Job Card", job_card)
doc.append("time_logs", { doc.time_logs[0].completed_qty = 1
"from_time": add_to_date(None, i),
"hours": 1,
"to_time": add_to_date(None, i + 1),
"completed_qty": doc.for_quantity
})
doc.submit() doc.submit()
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
@@ -518,6 +513,60 @@ class TestWorkOrder(unittest.TestCase):
work_order1.save() work_order1.save()
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
def test_batch_size_for_fg_item(self):
fg_item = "Test Batch Size Item For BOM 3"
rm1 = "Test Batch Size Item RM 1 For BOM 3"
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0)
for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]:
item_args = {
"include_item_in_manufacturing": 1,
"is_stock_item": 1
}
if item == fg_item:
item_args['has_batch_no'] = 1
item_args['create_new_batch'] = 1
item_args['batch_number_series'] = 'TBSI3.#####'
make_item(item, item_args)
bom_name = frappe.db.get_value("BOM",
{"item": fg_item, "is_active": 1, "with_operations": 1}, "name")
if not bom_name:
bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True)
bom.save()
bom.submit()
bom_name = bom.name
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1)
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
for row in ste1.get('items'):
if row.is_finished_item:
self.assertEqual(row.item_code, fg_item)
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1)
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1)
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
for row in ste1.get('items'):
if row.is_finished_item:
self.assertEqual(row.item_code, fg_item)
work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(),
qty=30, do_not_save = True)
work_order.batch_size = 10
work_order.insert()
work_order.submit()
self.assertEqual(work_order.has_batch_no, 1)
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30))
for row in ste1.get('items'):
if row.is_finished_item:
self.assertEqual(row.item_code, fg_item)
self.assertEqual(row.qty, 10)
frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0)
def test_partial_material_consumption(self): def test_partial_material_consumption(self):
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1)
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)

View File

@@ -64,11 +64,16 @@
"description", "description",
"stock_uom", "stock_uom",
"column_break2", "column_break2",
"references_section",
"material_request", "material_request",
"material_request_item", "material_request_item",
"sales_order_item", "sales_order_item",
"column_break_61",
"production_plan", "production_plan",
"production_plan_item", "production_plan_item",
"production_plan_sub_assembly_item",
"parent_work_order",
"bom_level",
"product_bundle_item", "product_bundle_item",
"amended_from" "amended_from"
], ],
@@ -546,17 +551,26 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 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", "icon": "fa fa-cogs",
"idx": 1, "idx": 1,
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-06-20 15:19:14.902699", "modified": "2021-06-28 16:19:14.902699",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",
"nsm_parent_field": "parent_work_order",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -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 # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe import frappe
import json import json
import math import math
@@ -30,9 +29,6 @@ class ItemHasVariantError(frappe.ValidationError): pass
class SerialNoQtyError(frappe.ValidationError): class SerialNoQtyError(frappe.ValidationError):
pass pass
form_grid_templates = {
"operations": "templates/form_grid/work_order_grid.html"
}
class WorkOrder(Document): class WorkOrder(Document):
def onload(self): def onload(self):
@@ -243,7 +239,7 @@ class WorkOrder(Document):
self.create_serial_no_batch_no() self.create_serial_no_batch_no()
def on_submit(self): def on_submit(self):
if not self.wip_warehouse: if not self.wip_warehouse and not self.skip_transfer:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
if not self.fg_warehouse: if not self.fg_warehouse:
frappe.throw(_("For Warehouse is required before Submit")) frappe.throw(_("For Warehouse is required before Submit"))
@@ -472,46 +468,47 @@ class WorkOrder(Document):
def set_work_order_operations(self): def set_work_order_operations(self):
"""Fetch operations from BOM and set in 'Work Order'""" """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 return
if self.use_multi_level_bom: operations = []
bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() 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: 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) 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() self.calculate_time()
def calculate_time(self): def calculate_time(self):
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
for d in self.get("operations"): 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() self.calculate_operating_cost()
@@ -593,6 +590,7 @@ class WorkOrder(Document):
def validate_operation_time(self): def validate_operation_time(self):
for d in self.operations: for d in self.operations:
if not d.time_in_mins > 0: 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)) frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
def update_required_items(self): def update_required_items(self):

View File

@@ -2,7 +2,6 @@
"actions": [], "actions": [],
"creation": "2014-10-16 14:35:41.950175", "creation": "2014-10-16 14:35:41.950175",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"details", "details",
@@ -49,6 +48,7 @@
{ {
"fieldname": "bom", "fieldname": "bom",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "BOM", "label": "BOM",
"no_copy": 1, "no_copy": 1,
"options": "BOM", "options": "BOM",
@@ -68,6 +68,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"columns": 1,
"description": "Operation completed for how many finished goods?", "description": "Operation completed for how many finished goods?",
"fieldname": "completed_qty", "fieldname": "completed_qty",
"fieldtype": "Float", "fieldtype": "Float",
@@ -77,6 +78,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 1,
"default": "Pending", "default": "Pending",
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
@@ -119,6 +121,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"columns": 1,
"description": "in Minutes", "description": "in Minutes",
"fieldname": "time_in_mins", "fieldname": "time_in_mins",
"fieldtype": "Float", "fieldtype": "Float",
@@ -205,7 +208,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-01-12 14:48:31.061286", "modified": "2021-06-24 14:36:12.835543",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",
@@ -214,4 +217,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -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']) fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom'])
for item in exploded_items: for item in exploded_items:
print(item.bom_no, indent)
item["indent"] = indent item["indent"] = indent
data.append({ data.append({
'item_code': item.item_code, 'item_code': item.item_code,
'item_name': item.item_name, 'item_name': item.item_name,
'indent': indent, 'indent': indent,
'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level")
if item.bom_no else ""),
'bom': item.bom_no, 'bom': item.bom_no,
'qty': item.qty * qty, 'qty': item.qty * qty,
'uom': item.uom, 'uom': item.uom,
'description': item.description, 'description': item.description,
'scrap': item.scrap 'scrap': item.scrap
}) })
if item.bom_no: if item.bom_no:
get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty) get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty)
@@ -68,6 +71,12 @@ def get_columns():
"fieldname": "uom", "fieldname": "uom",
"width": 100 "width": 100
}, },
{
"label": "BOM Level",
"fieldtype": "Data",
"fieldname": "bom_level",
"width": 100
},
{ {
"label": "Standard Description", "label": "Standard Description",
"fieldtype": "data", "fieldtype": "data",

View File

@@ -70,12 +70,12 @@ def get_bom_stock(filters):
ON bom_item.item_code = ledger.item_code ON bom_item.item_code = ledger.item_code
{conditions} {conditions}
WHERE 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( GROUP BY bom_item.item_code""".format(
qty_field=qty_field, qty_field=qty_field,
table=table, table=table,
conditions=conditions, conditions=conditions,
bom=bom, bom=frappe.db.escape(bom),
qty_to_produce=qty_to_produce or 1) qty_to_produce=qty_to_produce or 1)
) )

View File

@@ -68,6 +68,18 @@ frappe.query_reports["Job Card Summary"] = {
get_data: function(txt) { get_data: function(txt) {
return frappe.db.get_link_options('Item', 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"
} }
] ]
}; };

View File

@@ -1,14 +1,16 @@
{ {
"add_total_row": 0, "add_total_row": 1,
"columns": [],
"creation": "2020-04-20 12:00:21.436619", "creation": "2020-04-20 12:00:21.436619",
"disable_prepared_report": 0, "disable_prepared_report": 0,
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"filters": [],
"idx": 0, "idx": 0,
"is_standard": "Yes", "is_standard": "Yes",
"letter_head": "Gadgets International", "letter_head": "",
"modified": "2020-04-20 12:00:21.436619", "modified": "2020-12-30 11:49:21.713561",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card Summary", "name": "Job Card Summary",

View File

@@ -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;
},
};

View File

@@ -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"
}
]
}

View File

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

View File

@@ -19,7 +19,7 @@ def execute(filters=None):
return columns, data, None, chart_data return columns, data, None, chart_data
def get_data(filters): def get_data(filters):
query_filters = {"docstatus": 1} query_filters = {"docstatus": ("<", 2)}
fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty", fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty",
"planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"] "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, "Not Started": 0,
"In Process": 0, "In Process": 0,
"Stopped": 0, "Stopped": 0,
"Completed": 0 "Completed": 0,
"Draft": 0
} }
for d in data: for d in data:

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships

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

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

View File

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

View File

@@ -110,11 +110,11 @@ class AdditionalSalary(Document):
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days return amount_per_day * no_of_days
@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type): def get_additional_salaries(employee, start_date, end_date, component_type):
additional_salary_list = frappe.db.sql(""" additional_salary_list = frappe.db.sql("""
select name, salary_component as component, type, amount, select name, salary_component as component, type, amount, overwrite_salary_structure_amount as overwrite,
overwrite_salary_structure_amount as overwrite, deduct_full_tax_on_selected_payroll_date, is_recurring
deduct_full_tax_on_selected_payroll_date
from `tabAdditional Salary` from `tabAdditional Salary`
where employee=%(employee)s where employee=%(employee)s
and docstatus = 1 and docstatus = 1

View File

@@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', {
}); });
frm.set_query('employee', 'employees', () => { frm.set_query('employee', 'employees', () => {
if (!frm.doc.company) { let error_fields = [];
frappe.msgprint(__("Please set a Company")); let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date'];
return [];
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 { return {
query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query", query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query",
filters: frm.events.get_employee_filters(frm) filters: frm.events.get_employee_filters(frm)
@@ -148,25 +164,22 @@ frappe.ui.form.on('Payroll Entry', {
get_employee_filters: function (frm) { get_employee_filters: function (frm) {
let filters = {}; 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['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) { let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account',
filters['department'] = frm.doc.department; 'currency', 'department', 'branch', 'designation'];
}
if (frm.doc.branch) { fields.forEach(field => {
filters['branch'] = frm.doc.branch; if (frm.doc[field]) {
} filters[field] = frm.doc[field];
if (frm.doc.designation) { }
filters['designation'] = frm.doc.designation; });
}
if (frm.doc.employees) { 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; return filters;
}, },

View File

@@ -117,7 +117,6 @@ class PayrollEntry(Document):
Creates salary slip for selected employees if already not created Creates salary slip for selected employees if already not created
""" """
self.check_permission('write') self.check_permission('write')
self.created = 1
employees = [emp.employee for emp in self.employees] employees = [emp.employee for emp in self.employees]
if employees: if employees:
args = frappe._dict({ args = frappe._dict({
@@ -459,6 +458,7 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account):
where where
t1.name = t2.employee t1.name = t2.employee
and t2.docstatus = 1 and t2.docstatus = 1
and t1.status != 'Inactive'
%s order by t2.from_date desc %s order by t2.from_date desc
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
@@ -679,9 +679,13 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
conditions = [] conditions = []
include_employees = [] include_employees = []
emp_cond = '' emp_cond = ''
if not filters.payroll_frequency:
frappe.throw(_('Select Payroll Frequency.'))
if filters.start_date and filters.end_date: if filters.start_date and filters.end_date:
employee_list = get_employee_list(filters) 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] include_employees = [employee.employee for employee in employee_list if employee.employee not in emp]
filters.pop('start_date') filters.pop('start_date')
filters.pop('end_date') filters.pop('end_date')

View File

@@ -12,6 +12,7 @@
"year_to_date", "year_to_date",
"section_break_5", "section_break_5",
"additional_salary", "additional_salary",
"is_recurring_additional_salary",
"statistical_component", "statistical_component",
"depends_on_payment_days", "depends_on_payment_days",
"exempted_from_income_tax", "exempted_from_income_tax",
@@ -235,11 +236,19 @@
"label": "Year To Date", "label": "Year To Date",
"options": "currency", "options": "currency",
"read_only": 1 "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, "istable": 1,
"links": [], "links": [],
"modified": "2021-01-14 13:39:15.847158", "modified": "2021-03-14 13:39:15.847158",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Detail", "name": "Salary Detail",

View File

@@ -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.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.model.naming import make_autoname
from frappe.utils.background_jobs import enqueue
from frappe import msgprint, _ from frappe import msgprint, _
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.utilities.transaction_base import TransactionBase 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.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.payroll_period.payroll_period import get_period_factor, get_payroll_period
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
@@ -616,7 +616,8 @@ class SalarySlip(TransactionBase):
get_salary_component_data(additional_salary.component), get_salary_component_data(additional_salary.component),
additional_salary.amount, additional_salary.amount,
component_type, component_type,
additional_salary additional_salary,
is_recurring = additional_salary.is_recurring
) )
def add_tax_components(self, payroll_period): def add_tax_components(self, payroll_period):
@@ -637,7 +638,7 @@ class SalarySlip(TransactionBase):
tax_row = get_salary_component_data(d) tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions") 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 component_row = None
for d in self.get(component_type): for d in self.get(component_type):
if d.salary_component != component_data.salary_component: if d.salary_component != component_data.salary_component:
@@ -678,6 +679,7 @@ class SalarySlip(TransactionBase):
component_row.set('abbr', abbr) component_row.set('abbr', abbr)
if additional_salary: if additional_salary:
component_row.is_recurring_additional_salary = is_recurring
component_row.default_amount = 0 component_row.default_amount = 0
component_row.additional_amount = amount component_row.additional_amount = amount
component_row.additional_salary = additional_salary.name 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) # get remaining numbers of sub-period (period for which one salary is processed)
remaining_sub_periods = get_period_factor(self.employee, remaining_sub_periods = get_period_factor(self.employee,
self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1]
# get taxable_earnings, paid_taxes for previous period # get taxable_earnings, paid_taxes for previous period
previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date,
self.start_date, tax_slab.allow_tax_exemption) self.start_date, tax_slab.allow_tax_exemption)
@@ -870,8 +873,16 @@ class SalarySlip(TransactionBase):
if earning.is_tax_applicable: if earning.is_tax_applicable:
if additional_amount: if additional_amount:
taxable_earnings += (amount - additional_amount) if not earning.is_recurring_additional_salary:
additional_income += additional_amount 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: if earning.deduct_full_tax_on_selected_payroll_date:
additional_income_with_full_tax += additional_amount additional_income_with_full_tax += additional_amount
continue continue
@@ -1091,6 +1102,7 @@ class SalarySlip(TransactionBase):
"applicant": self.employee, "applicant": self.employee,
"docstatus": 1, "docstatus": 1,
"repay_from_salary": 1, "repay_from_salary": 1,
"company": self.company
}) })
def make_loan_repayment_entry(self): def make_loan_repayment_entry(self):

View File

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

View File

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

View File

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

View File

@@ -67,6 +67,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
calculate_discount_amount: function(){ calculate_discount_amount: function(){
if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) { if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) {
this.calculate_item_values();
this.calculate_net_total();
this.set_discount_amount(); this.set_discount_amount();
this.apply_discount_amount(); this.apply_discount_amount();
} }

View File

@@ -54,7 +54,7 @@ frappe.help.help_links["permission-manager"] = [
frappe.help.help_links["Form/System Settings"] = [ frappe.help.help_links["Form/System Settings"] = [
{ {
label: "Naming Series", label: "System Settings",
url: docsUrl + "user/manual/en/setting-up/settings/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", label: "PayPal Settings",
url: url:
docsUrl + 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", label: "Razorpay Settings",
url: url:
docsUrl + docsUrl +
"user/manual/en/setting-up/integrations/razorpay-integration", "user/manual/en/erpnext_integration/razorpay-integration",
}, },
]; ];
frappe.help.help_links["Form/Dropbox Settings"] = [ frappe.help.help_links["Form/Dropbox Settings"] = [
{ {
label: "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", label: "LDAP Settings",
url: 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", label: "Stripe Settings",
url: url:
docsUrl + 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", label: "Nested BOM Structure",
url: url:
docsUrl + docsUrl +
"user/manual/en/manufacturing/articles/nested-bom-structure", "user/manual/en/manufacturing/articles/managing-multi-level-bom",
}, },
]; ];

View File

@@ -147,7 +147,7 @@ erpnext.setup.slides_settings = [
} }
// Validate bank name // Validate bank name
if(me.values.bank_account){ if(me.values.bank_account) {
frappe.call({ frappe.call({
async: false, async: false,
method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account", method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account",

View File

@@ -1,9 +1,9 @@
frappe.provide('frappe.ui.form'); frappe.provide('frappe.ui.form');
frappe.ui.form.CustomerQuickEntryForm = frappe.ui.form.QuickEntryForm.extend({ 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.skip_redirect_on_error = true;
this._super(doctype, after_insert);
}, },
render_dialog: function() { render_dialog: function() {

View File

@@ -35,6 +35,7 @@ frappe.ui.form.on('GST Settings', {
return { return {
filters: { filters: {
company: row.company, company: row.company,
account_type: "Tax",
is_group: 0 is_group: 0
} }
}; };

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