Merge branch 'develop' into enable-discount-accounting

This commit is contained in:
Ganga Manoj 2021-07-17 20:01:09 +05:30 committed by GitHub
commit bfb7256143
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 3866 additions and 2235 deletions

View File

@ -42,6 +42,6 @@ sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext "${GITHUB_WORKSPACE}"
bench start &
bench start &> bench_run_logs.txt &
bench --site test_site reinstall --yes
bench build --app frappe

View File

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

View File

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

View File

@ -102,3 +102,7 @@ jobs:
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
- name: Show bench console if tests failed
if: ${{ failure() }}
run: cat ~/frappe-bench/bench_run_logs.txt

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ __pycache__
.idea/
.vscode/
node_modules/
.backportrc.json

View File

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

View File

@ -0,0 +1,44 @@
describe("Test Item Dashboard", () => {
before(() => {
cy.login();
cy.visit("/app/item");
cy.insert_doc(
"Item",
{
item_code: "e2e_test_item",
item_group: "All Item Groups",
opening_stock: 42,
valuation_rate: 100,
},
true
);
cy.go_to_doc("item", "e2e_test_item");
});
it("should show dashboard with correct data on first load", () => {
cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
cy.get(".stock-levels").contains("e2e_test_item").should("exist");
// reserved and available qty
cy.get(".stock-levels .inline-graph-count")
.eq(0)
.contains("0")
.should("exist");
cy.get(".stock-levels .inline-graph-count")
.eq(1)
.contains("42")
.should("exist");
});
it("should persist on field change", () => {
cy.get('input[data-fieldname="disabled"]').check();
cy.wait(500);
cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
cy.get(".stock-levels").should("have.length", 1);
});
it("should persist on reload", () => {
cy.reload();
cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
});
});

View File

@ -23,3 +23,9 @@
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
const slug = (name) => name.toLowerCase().replace(" ", "-");
Cypress.Commands.add("go_to_doc", (doctype, name) => {
cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`);
});

View File

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

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

View File

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

View File

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

View File

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

View File

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

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)
self.assertEqual(pe.cost_center, si.cost_center)
self.assertEqual(expected_account_balance, account_balance)
self.assertEqual(expected_party_balance, party_balance)
self.assertEqual(expected_party_account_balance, party_account_balance)
self.assertEqual(flt(expected_account_balance), account_balance)
self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def create_payment_terms_template():

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ from erpnext.accounts.utils import get_account_currency
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
from erpnext.assets.doctype.asset.depreciation \
import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal
import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_delivery_note_serial_no
from erpnext.setup.doctype.company.company import update_company_current_month_sales
@ -149,7 +149,7 @@ class SalesInvoice(SellingController):
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
elif asset.status in ("Scrapped", "Cancelled", "Sold"):
elif asset.status in ("Scrapped", "Cancelled") or (asset.status == "Sold" and not self.is_return):
frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status))
def validate_item_cost_centers(self):
@ -840,6 +840,7 @@ class SalesInvoice(SellingController):
self.make_customer_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
@ -923,22 +924,33 @@ class SalesInvoice(SellingController):
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")):
if item.is_fixed_asset:
asset = frappe.get_doc("Asset", item.asset)
if item.get('asset'):
asset = frappe.get_doc("Asset", item.asset)
else:
frappe.throw(_(
"Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
title=_("Missing Asset")
)
if (len(asset.finance_books) > 1 and not item.finance_book
and asset.finance_books[0].finance_book):
frappe.throw(_("Select finance book for the item {0} at row {1}")
.format(item.item_code, item.idx))
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
item.base_net_amount, item.finance_book)
if self.is_return:
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset,
item.base_net_amount, item.finance_book)
asset.db_set("disposal_date", None)
else:
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
item.base_net_amount, item.finance_book)
asset.db_set("disposal_date", self.posting_date)
for gle in fixed_asset_gl_entries:
gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))
asset.db_set("disposal_date", self.posting_date)
asset.set_status("Sold" if self.docstatus==1 else None)
self.set_asset_status(asset)
else:
# Do not book income for transfer within same company
if not self.is_internal_transfer():
@ -964,6 +976,12 @@ class SalesInvoice(SellingController):
erpnext.is_perpetual_inventory_enabled(self.company):
gl_entries += super(SalesInvoice, self).get_gl_entries()
def set_asset_status(self, asset):
if self.is_return:
asset.set_status()
else:
asset.set_status("Sold" if self.docstatus==1 else None)
def make_loyalty_point_redemption_gle(self, gl_entries):
if cint(self.redeem_loyalty_points):
gl_entries.append(

View File

@ -10,6 +10,7 @@ from frappe.model.dynamic_links import get_dynamic_link_map
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from frappe.model.naming import make_autoname
@ -1069,6 +1070,36 @@ class TestSalesInvoice(unittest.TestCase):
self.assertFalse(si1.outstanding_amount)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
def test_gle_made_when_asset_is_returned(self):
create_asset_data()
asset = create_asset(item_code="Macbook Pro")
si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000)
return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="Macbook Pro", asset=asset.name, qty=-1, rate=90000)
disposal_account = frappe.get_cached_value("Company", "_Test Company", "disposal_account")
# Asset value is 100,000 but it was sold for 90,000, so there should be a loss of 10,000
loss_for_si = frappe.get_all(
"GL Entry",
filters = {
"voucher_no": si.name,
"account": disposal_account
},
fields = ["credit", "debit"]
)[0]
loss_for_return_si = frappe.get_all(
"GL Entry",
filters = {
"voucher_no": return_si.name,
"account": disposal_account
},
fields = ["credit", "debit"]
)[0]
self.assertEqual(loss_for_si['credit'], loss_for_return_si['debit'])
self.assertEqual(loss_for_si['debit'], loss_for_return_si['credit'])
def test_discount_on_net_total(self):
si = frappe.copy_doc(test_records[2])
@ -2133,9 +2164,9 @@ def make_sales_invoice_for_ewaybill():
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company",
"cgst_account": "CGST - _TC",
"sgst_account": "SGST - _TC",
"igst_account": "IGST - _TC",
"cgst_account": "Output Tax CGST - _TC",
"sgst_account": "Output Tax SGST - _TC",
"igst_account": "Output Tax IGST - _TC",
})
gst_settings.save()
@ -2152,7 +2183,7 @@ def make_sales_invoice_for_ewaybill():
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "CGST - _TC",
"account_head": "Output Tax CGST - _TC",
"cost_center": "Main - _TC",
"description": "CGST @ 9.0",
"rate": 9
@ -2160,7 +2191,7 @@ def make_sales_invoice_for_ewaybill():
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "SGST - _TC",
"account_head": "Output Tax SGST - _TC",
"cost_center": "Main - _TC",
"description": "SGST @ 9.0",
"rate": 9
@ -2213,6 +2244,7 @@ def create_sales_invoice(**args):
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"discount_account": args.discount_account or None,
"discount_amount": args.discount_amount or 0,
"asset": args.asset or None,
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"conversion_factor": 1

View File

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

View File

@ -744,7 +744,6 @@
"fieldname": "asset",
"fieldtype": "Link",
"label": "Asset",
"no_copy": 1,
"options": "Asset"
},
{

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -176,22 +176,34 @@ def restore_asset(asset_name):
asset.set_status()
@frappe.whitelist()
def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \
get_asset_details(asset, finance_book)
gl_entries = [
{
"account": fixed_asset_account,
"debit_in_account_currency": asset.gross_purchase_amount,
"debit": asset.gross_purchase_amount,
"cost_center": depreciation_cost_center
},
{
"account": accumulated_depr_account,
"credit_in_account_currency": accumulated_depr_amount,
"credit": accumulated_depr_amount,
"cost_center": depreciation_cost_center
}
]
profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
if profit_amount:
get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
return gl_entries
def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None):
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset)
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
depreciation_cost_center = asset.cost_center or depreciation_cost_center
idx = 1
if finance_book:
for d in asset.finance_books:
if d.finance_book == finance_book:
idx = d.idx
break
value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation
if asset.calculate_depreciation else asset.value_after_depreciation)
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \
get_asset_details(asset, finance_book)
gl_entries = [
{
@ -210,16 +222,37 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None)
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
if profit_amount:
debit_or_credit = "debit" if profit_amount < 0 else "credit"
gl_entries.append({
"account": disposal_account,
"cost_center": depreciation_cost_center,
debit_or_credit: abs(profit_amount),
debit_or_credit + "_in_account_currency": abs(profit_amount)
})
get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center)
return gl_entries
def get_asset_details(asset, finance_book=None):
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset)
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
depreciation_cost_center = asset.cost_center or depreciation_cost_center
idx = 1
if finance_book:
for d in asset.finance_books:
if d.finance_book == finance_book:
idx = d.idx
break
value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation
if asset.calculate_depreciation else asset.value_after_depreciation)
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
return fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation
def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center):
debit_or_credit = "debit" if profit_amount < 0 else "credit"
gl_entries.append({
"account": disposal_account,
"cost_center": depreciation_cost_center,
debit_or_credit: abs(profit_amount),
debit_or_credit + "_in_account_currency": abs(profit_amount)
})
@frappe.whitelist()
def get_disposal_account_and_cost_center(company):
disposal_account, depreciation_cost_center = frappe.get_cached_value('Company', company,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -102,7 +102,7 @@
}
],
"links": [],
"modified": "2021-06-30 12:09:14.228756",
"modified": "2021-06-30 13:09:14.228756",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",

View File

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

View File

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

View File

@ -245,7 +245,10 @@ doc_events = {
"erpnext.portal.utils.set_default_role"]
},
"Communication": {
"on_update": "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time"
"on_update": [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time",
"erpnext.support.doctype.issue.issue.set_first_response_time"
]
},
("Sales Taxes and Charges Template", 'Price List'): {
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"

View File

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

View File

@ -33,7 +33,8 @@ frappe.ui.form.on('Training Event', {
frm.set_query("employee", "employees", function () {
return {
filters: {
name: ["NOT IN", emp]
name: ["NOT IN", emp],
status: "Active"
}
};
});

View File

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

View File

@ -14,11 +14,18 @@ frappe.ui.form.on('Loan Application', {
refresh: function(frm) {
frm.trigger("toggle_fields");
frm.trigger("add_toolbar_buttons");
frm.set_query("loan_type", () => {
return {
filters: {
company: frm.doc.company
}
};
});
},
repayment_method: function(frm) {
frm.doc.repayment_amount = frm.doc.repayment_periods = ""
frm.trigger("toggle_fields")
frm.trigger("toggle_required")
frm.doc.repayment_amount = frm.doc.repayment_periods = "";
frm.trigger("toggle_fields");
frm.trigger("toggle_required");
},
toggle_fields: function(frm) {
frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -154,6 +154,7 @@ class BOM(WebsiteGenerator):
self.calculate_cost()
self.update_stock_qty()
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):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
@ -676,6 +677,19 @@ class BOM(WebsiteGenerator):
"""Get a complete tree representation preserving order of child items."""
return BOMTree(self.name)
def set_bom_level(self, update=False):
levels = []
self.bom_level = 0
for row in self.items:
if row.bom_no:
levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0)
if levels:
self.bom_level = max(levels) + 1
if update:
self.db_set("bom_level", self.bom_level)
def get_bom_item_rate(args, bom_doc):
if bom_doc.rm_cost_as_per == 'Valuation Rate':
@ -699,7 +713,8 @@ def get_bom_item_rate(args, bom_doc):
"conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function
"conversion_factor": args.get("conversion_factor") or 1,
"plc_conversion_rate": 1,
"ignore_party": True
"ignore_party": True,
"ignore_conversion_rate": True
})
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
out = frappe._dict()
@ -860,7 +875,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
frappe.form_dict.parent = parent
if frappe.form_dict.parent:
bom_doc = frappe.get_doc("BOM", frappe.form_dict.parent)
bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent)
frappe.has_permission("BOM", doc=bom_doc, throw=True)
bom_items = frappe.get_all('BOM Item',
@ -871,7 +886,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
item_names = tuple(d.get('item_code') for d in bom_items)
items = frappe.get_list('Item',
fields=['image', 'description', 'name', 'stock_uom', 'item_name'],
fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'],
filters=[['name', 'in', item_names]]) # to get only required item dicts
for bom_item in bom_items:
@ -884,6 +899,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
bom_item.parent_bom_qty = bom_doc.quantity
bom_item.expandable = 0 if bom_item.value in ('', None) else 1
bom_item.image = frappe.db.escape(bom_item.image)
return bom_items

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -64,11 +64,16 @@
"description",
"stock_uom",
"column_break2",
"references_section",
"material_request",
"material_request_item",
"sales_order_item",
"column_break_61",
"production_plan",
"production_plan_item",
"production_plan_sub_assembly_item",
"parent_work_order",
"bom_level",
"product_bundle_item",
"amended_from"
],
@ -546,17 +551,26 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
},
{
"fieldname": "production_plan_sub_assembly_item",
"fieldtype": "Data",
"label": "Production Plan Sub-assembly Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-cogs",
"idx": 1,
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2021-06-20 15:19:14.902699",
"modified": "2021-06-28 16:19:14.902699",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
"nsm_parent_field": "parent_work_order",
"owner": "Administrator",
"permissions": [
{

View File

@ -239,7 +239,7 @@ class WorkOrder(Document):
self.create_serial_no_batch_no()
def on_submit(self):
if not self.wip_warehouse:
if not self.wip_warehouse and not self.skip_transfer:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
if not self.fg_warehouse:
frappe.throw(_("For Warehouse is required before Submit"))
@ -483,7 +483,7 @@ class WorkOrder(Document):
self.set('operations', [])
if not self.bom_no:
if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'):
return
operations = []
@ -590,6 +590,7 @@ class WorkOrder(Document):
def validate_operation_time(self):
for d in self.operations:
if not d.time_in_mins > 0:
print(self.bom_no, self.production_item)
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
def update_required_items(self):

View File

@ -0,0 +1,33 @@
<div style="padding: 15px;">
<div class="row mb-5">
<div class="col-md-5" style="max-height: 500px">
{% if data.image %}
<div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
<img class="responsive" src={{ data.image }}>
</div>
{% endif %}
</div>
<div class="col-md-7 h-500">
<div style="padding-top: 10px;">
<b> Status </b> {{ data.status }}
</div>
<div style="padding-top: 10px;">
<b> Qty to Produce </b> {{ data.qty }}
</div>
<div style="padding-top: 10px;">
<b> Produced Qty </b> {{ data.produced_qty }}
</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/Work Order/{{ data.value }}">
{{ __("Open Work Order {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>

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

View File

@ -70,12 +70,12 @@ def get_bom_stock(filters):
ON bom_item.item_code = ledger.item_code
{conditions}
WHERE
bom_item.parent = '{bom}' and bom_item.parenttype='BOM'
bom_item.parent = {bom} and bom_item.parenttype='BOM'
GROUP BY bom_item.item_code""".format(
qty_field=qty_field,
table=table,
conditions=conditions,
bom=bom,
bom=frappe.db.escape(bom),
qty_to_produce=qty_to_produce or 1)
)

View File

@ -68,6 +68,18 @@ frappe.query_reports["Job Card Summary"] = {
get_data: function(txt) {
return frappe.db.get_link_options('Item', txt);
}
},
{
label: __("Workstation"),
fieldname: "workstation",
fieldtype: "Link",
options: "Workstation"
},
{
label: __("Operation"),
fieldname: "operation",
fieldtype: "Link",
options: "Operation"
}
]
};

View File

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

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

View File

@ -290,5 +290,8 @@ erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.update_response_by_variance
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry

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,31 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
if frappe.db.exists('DocType', 'Issue') and frappe.db.count('Issue'):
invalid_issues = frappe.get_all('Issue', {
'first_responded_on': ['is', 'set'],
'response_by_variance': ['<', 0]
}, ["name", "response_by_variance", "timestampdiff(Second, `first_responded_on`, `response_by`) as variance"])
# issues which has response_by_variance set as -ve
# but diff between first_responded_on & response_by is +ve i.e SLA isn't failed
invalid_issues = [d for d in invalid_issues if d.get('variance') > 0]
for issue in invalid_issues:
frappe.db.set_value('Issue', issue.get('name'), 'response_by_variance', issue.get('variance'), update_modified=False)
invalid_issues = frappe.get_all('Issue', {
'resolution_date': ['is', 'set'],
'resolution_by_variance': ['<', 0]
}, ["name", "resolution_by_variance", "timestampdiff(Second, `resolution_date`, `resolution_by`) as variance"])
# issues which has resolution_by_variance set as -ve
# but diff between resolution_date & resolution_by is +ve i.e SLA isn't failed
invalid_issues = [d for d in invalid_issues if d.get('variance') > 0]
for issue in invalid_issues:
frappe.db.set_value('Issue', issue.get('name'), 'resolution_by_variance', issue.get('variance'), update_modified=False)

View File

@ -117,7 +117,6 @@ class PayrollEntry(Document):
Creates salary slip for selected employees if already not created
"""
self.check_permission('write')
self.created = 1
employees = [emp.employee for emp in self.employees]
if employees:
args = frappe._dict({
@ -686,7 +685,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
if filters.start_date and filters.end_date:
employee_list = get_employee_list(filters)
emp = filters.get('employees')
emp = filters.get('employees') or []
include_employees = [employee.employee for employee in employee_list if employee.employee not in emp]
filters.pop('start_date')
filters.pop('end_date')

View File

@ -1088,6 +1088,7 @@ class SalarySlip(TransactionBase):
"applicant": self.employee,
"docstatus": 1,
"repay_from_salary": 1,
"company": self.company
})
def make_loan_repayment_entry(self):

View File

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

View File

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

View File

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

View File

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

View File

@ -77,9 +77,6 @@ class Task(NestedSet):
if flt(self.progress or 0) > 100:
frappe.throw(_("Progress % for a task cannot be more than 100."))
if flt(self.progress) == 100:
self.status = 'Completed'
if self.status == 'Completed':
self.progress = 100

View File

@ -67,6 +67,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_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.apply_discount_amount();
}

View File

@ -54,7 +54,7 @@ frappe.help.help_links["permission-manager"] = [
frappe.help.help_links["Form/System Settings"] = [
{
label: "Naming Series",
label: "System Settings",
url: docsUrl + "user/manual/en/setting-up/settings/system-settings",
},
];
@ -206,7 +206,7 @@ frappe.help.help_links["Form/PayPal Settings"] = [
label: "PayPal Settings",
url:
docsUrl +
"user/manual/en/setting-up/integrations/paypal-integration",
"user/manual/en/erpnext_integration/paypal-integration",
},
];
@ -215,14 +215,14 @@ frappe.help.help_links["Form/Razorpay Settings"] = [
label: "Razorpay Settings",
url:
docsUrl +
"user/manual/en/setting-up/integrations/razorpay-integration",
"user/manual/en/erpnext_integration/razorpay-integration",
},
];
frappe.help.help_links["Form/Dropbox Settings"] = [
{
label: "Dropbox Settings",
url: docsUrl + "user/manual/en/setting-up/integrations/dropbox-backup",
url: docsUrl + "user/manual/en/erpnext_integration/dropbox-backup",
},
];
@ -230,7 +230,7 @@ frappe.help.help_links["Form/LDAP Settings"] = [
{
label: "LDAP Settings",
url:
docsUrl + "user/manual/en/setting-up/integrations/ldap-integration",
docsUrl + "user/manual/en/erpnext_integration/ldap-integration",
},
];
@ -239,7 +239,7 @@ frappe.help.help_links["Form/Stripe Settings"] = [
label: "Stripe Settings",
url:
docsUrl +
"user/manual/en/setting-up/integrations/stripe-integration",
"user/manual/en/erpnext_integration/stripe-integration",
},
];
@ -991,7 +991,7 @@ frappe.help.help_links["Form/BOM"] = [
label: "Nested BOM Structure",
url:
docsUrl +
"user/manual/en/manufacturing/articles/nested-bom-structure",
"user/manual/en/manufacturing/articles/managing-multi-level-bom",
},
];

View File

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

View File

@ -19,6 +19,21 @@ class GSTSettings(Document):
from tabAddress where country = "India" and ifnull(gstin, '')!='' ''')
self.set_onload('data', data)
def validate(self):
# Validate duplicate accounts
self.validate_duplicate_accounts()
def validate_duplicate_accounts(self):
account_list = []
for account in self.get('gst_accounts'):
for fieldname in ['cgst_account', 'sgst_account', 'igst_account', 'cess_account']:
if account.get(fieldname) in account_list:
frappe.throw(_("Account {0} appears multiple times").format(
frappe.bold(account.get(fieldname))))
if account.get(fieldname):
account_list.append(account.get(fieldname))
@frappe.whitelist()
def send_reminder():
frappe.has_permission('GST Settings', throw=True)

View File

@ -46,14 +46,14 @@ class TestGSTR3BReport(unittest.TestCase):
make_sales_invoice()
create_purchase_invoices()
if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing"):
report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing")
if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing"):
report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing")
report.save()
else:
report = frappe.get_doc({
"doctype": "GSTR 3B Report",
"company": "_Test Company GST",
"company_address": "_Test Address-Billing",
"company_address": "_Test Address GST-Billing",
"year": getdate().year,
"month": month_number_mapping.get(getdate().month)
}).insert()
@ -89,7 +89,7 @@ class TestGSTR3BReport(unittest.TestCase):
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "IGST - _GST",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@ -117,7 +117,7 @@ def make_sales_invoice():
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "IGST - _GST",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@ -138,7 +138,7 @@ def make_sales_invoice():
si1.append("taxes", {
"charge_type": "On Net Total",
"account_head": "IGST - _GST",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@ -159,7 +159,7 @@ def make_sales_invoice():
si2.append("taxes", {
"charge_type": "On Net Total",
"account_head": "IGST - _GST",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@ -195,7 +195,7 @@ def create_purchase_invoices():
pi.append("taxes", {
"charge_type": "On Net Total",
"account_head": "CGST - _GST",
"account_head": "Input Tax CGST - _GST",
"cost_center": "Main - _GST",
"description": "CGST @ 9.0",
"rate": 9
@ -203,7 +203,7 @@ def create_purchase_invoices():
pi.append("taxes", {
"charge_type": "On Net Total",
"account_head": "SGST - _GST",
"account_head": "Input Tax SGST - _GST",
"cost_center": "Main - _GST",
"description": "SGST @ 9.0",
"rate": 9
@ -410,10 +410,10 @@ def make_company():
company.country = "India"
company.insert()
if not frappe.db.exists('Address', '_Test Address-Billing'):
if not frappe.db.exists('Address', '_Test Address GST-Billing'):
address = frappe.get_doc({
"address_title": "_Test Address GST",
"address_line1": "_Test Address Line 1",
"address_title": "_Test Address",
"address_type": "Billing",
"city": "_Test City",
"state": "Test State",
@ -444,9 +444,9 @@ def set_account_heads():
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company GST",
"cgst_account": "CGST - _GST",
"sgst_account": "SGST - _GST",
"igst_account": "IGST - _GST",
"cgst_account": "Output Tax CGST - _GST",
"sgst_account": "Output Tax SGST - _GST",
"igst_account": "Output Tax IGST - _GST"
})
gst_settings.save()

View File

@ -1,6 +1,8 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
async refresh(frm) {
if (frm.doc.docstatus == 2) return;
const res = await frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
args: { doc: frm.doc }
@ -111,7 +113,7 @@ erpnext.setup_einvoice_actions = (doctype) => {
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const action = () => {
let message = __('Cancellation of e-way bill is currently not supported. ');
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
message += '<br><br>';
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');

View File

@ -42,7 +42,10 @@ def validate_eligibility(doc):
invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
no_taxes_applied = not doc.get('taxes')
# if export invoice, then taxes can be empty
# invoice can only be ineligible if no taxes applied and is not an export invoice
no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas'
has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst'))
if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item:
@ -188,9 +191,10 @@ def get_item_list(invoice):
item.qty = abs(item.qty)
item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty)
item.gross_amount = abs(item.taxable_value) + item.discount_amount
item.unit_rate = abs(item.taxable_value / item.qty)
item.gross_amount = abs(item.taxable_value)
item.taxable_value = abs(item.taxable_value)
item.discount_amount = 0
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None

View File

@ -12,7 +12,10 @@ from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
from frappe.utils import today
def setup(company=None, patch=True):
setup_company_independent_fixtures(patch=patch)
# Company independent fixtures should be called only once at the first company setup
if frappe.db.count('Company', {'country': 'India'}) <=1:
setup_company_independent_fixtures(patch=patch)
if not patch:
make_fixtures(company)
@ -25,6 +28,7 @@ def setup_company_independent_fixtures(patch=False):
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
create_gratuity_rule()
add_print_formats()
update_accounts_settings_for_taxes()
def add_hsn_sac_codes():
if frappe.flags.in_test and frappe.flags.created_hsn_codes:
@ -121,10 +125,12 @@ def add_print_formats():
def make_property_setters(patch=False):
# GST rules do not allow for an invoice no. bigger than 16 characters
journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
sales_invoice_series = ['SINV-.YY.-', 'SRET-.YY.-', ''] + frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n")
purchase_invoice_series = ['PINV-.YY.-', 'PRET-.YY.-', ''] + frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n")
if not patch:
make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '')
make_property_setter('Purchase Invoice', 'naming_series', 'options', '\n'.join(purchase_invoice_series), '')
make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
def make_custom_fields(update=True):
@ -680,7 +686,7 @@ def make_custom_fields(update=True):
def make_fixtures(company=None):
docs = []
company = company.name if company else frappe.db.get_value("Global Defaults", None, "default_company")
company = company or frappe.db.get_value("Global Defaults", None, "default_company")
set_salary_components(docs)
set_tds_account(docs, company)
@ -698,6 +704,53 @@ def make_fixtures(company=None):
# create records for Tax Withholding Category
set_tax_withholding_category(company)
def update_regional_tax_settings(country, company):
# Will only add default GST accounts if present
input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST']
output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST']
rcm_accounts = ['Input Tax CGST RCM', 'Input Tax SGST RCM', 'Input Tax IGST RCM']
gst_settings = frappe.get_single('GST Settings')
existing_account_list = []
for account in gst_settings.get('gst_accounts'):
for key in ['cgst_account', 'sgst_account', 'igst_account']:
existing_account_list.append(account.get(key))
gst_accounts = frappe._dict(frappe.get_all("Account",
{'company': company, 'account_name': ('in', input_account_names +
output_account_names + rcm_accounts)}, ['account_name', 'name'], as_list=1))
add_accounts_in_gst_settings(company, input_account_names, gst_accounts,
existing_account_list, gst_settings)
add_accounts_in_gst_settings(company, output_account_names, gst_accounts,
existing_account_list, gst_settings)
add_accounts_in_gst_settings(company, rcm_accounts, gst_accounts,
existing_account_list, gst_settings, is_reverse_charge=1)
gst_settings.save()
def add_accounts_in_gst_settings(company, account_names, gst_accounts,
existing_account_list, gst_settings, is_reverse_charge=0):
accounts_not_added = 1
for account in account_names:
# Default Account Added does not exists
if not gst_accounts.get(account):
accounts_not_added = 0
# Check if already added in GST Settings
if gst_accounts.get(account) in existing_account_list:
accounts_not_added = 0
if accounts_not_added:
gst_settings.append('gst_accounts', {
'company': company,
'cgst_account': gst_accounts.get(account_names[0]),
'sgst_account': gst_accounts.get(account_names[1]),
'igst_account': gst_accounts.get(account_names[2]),
'is_reverse_charge_account': is_reverse_charge
})
def set_salary_components(docs):
docs.extend([
{'doctype': 'Salary Component', 'salary_component': 'Professional Tax',
@ -731,13 +784,14 @@ def set_tax_withholding_category(company):
docs = get_tds_details(accounts, fiscal_year)
for d in docs:
try:
if not frappe.db.exists("Tax Withholding Category", d.get("name")):
doc = frappe.get_doc(d)
doc.flags.ignore_validate = True
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.insert()
except frappe.DuplicateEntryError:
doc = frappe.get_doc("Tax Withholding Category", d.get("name"))
else:
doc = frappe.get_doc("Tax Withholding Category", d.get("name"), for_update=True)
if accounts:
doc.append("accounts", accounts[0])
@ -749,11 +803,12 @@ def set_tax_withholding_category(company):
doc.append("rates", d.get('rates')[0])
doc.flags.ignore_permissions = True
doc.flags.ignore_validate = True
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True
doc.save()
def set_tds_account(docs, company):
abbr = frappe.get_value("Company", company, "abbr")
parent_account = frappe.db.get_value("Account", filters = {"account_name": "Duties and Taxes", "company": company})
if parent_account:
docs.extend([
@ -912,7 +967,6 @@ def get_tds_details(accounts, fiscal_year):
]
def create_gratuity_rule():
# Standard Indain Gratuity Rule
if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"):
rule = frappe.new_doc("Gratuity Rule")
@ -930,3 +984,7 @@ def create_gratuity_rule():
rule.flags.ignore_mandatory = True
rule.save()
def update_accounts_settings_for_taxes():
if frappe.db.count('Company') == 1:
frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0)

View File

@ -11,7 +11,7 @@
"is_standard": "Yes",
"json": "{}",
"letter_head": "Logo",
"modified": "2021-03-12 12:36:48.689413",
"modified": "2021-03-13 12:36:48.689413",
"modified_by": "Administrator",
"module": "Regional",
"name": "E-Invoice Summary",

View File

@ -584,7 +584,7 @@ class Gstr1Report(object):
def get_json(filters, report_name, data):
filters = json.loads(filters)
report_data = json.loads(data)
gstin = get_company_gstin_number(filters["company"], filters["company_address"])
gstin = get_company_gstin_number(filters.get("company"), filters.get("company_address"))
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)

View File

@ -130,6 +130,10 @@ frappe.ui.form.on("Customer", {
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
}, __('Create'));
frm.add_custom_button(__('Get Customer Group Details'), function () {
frm.trigger("get_customer_group_details");
}, __('Actions'));
// indicator
erpnext.utils.set_party_dashboard_indicators(frm);
@ -145,4 +149,15 @@ frappe.ui.form.on("Customer", {
if(frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name);
},
});
get_customer_group_details: function(frm) {
frappe.call({
method: "get_customer_group_details",
doc: frm.doc,
callback: function() {
frm.refresh();
}
});
}
});

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