mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-19 04:59:18 +00:00
Merge pull request #34532 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
5
.github/helper/install.sh
vendored
5
.github/helper/install.sh
vendored
@@ -8,8 +8,9 @@ sudo apt update && sudo apt install redis-server libcups2-dev
|
|||||||
|
|
||||||
pip install frappe-bench
|
pip install frappe-bench
|
||||||
|
|
||||||
|
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
||||||
frappeuser=${FRAPPE_USER:-"frappe"}
|
frappeuser=${FRAPPE_USER:-"frappe"}
|
||||||
frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
|
frappebranch=${FRAPPE_BRANCH:-$githubbranch}
|
||||||
|
|
||||||
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
|
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
|
||||||
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
|
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
|
||||||
@@ -56,7 +57,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile
|
|||||||
sed -i 's/socketio:/# socketio:/g' Procfile
|
sed -i 's/socketio:/# socketio:/g' Procfile
|
||||||
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
|
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
|
||||||
|
|
||||||
bench get-app payments
|
bench get-app payments --branch ${githubbranch%"-hotfix"}
|
||||||
bench get-app erpnext "${GITHUB_WORKSPACE}"
|
bench get-app erpnext "${GITHUB_WORKSPACE}"
|
||||||
|
|
||||||
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
||||||
|
|||||||
@@ -393,7 +393,13 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
|||||||
|
|
||||||
if ancestors and not allow_independent_account_creation:
|
if ancestors and not allow_independent_account_creation:
|
||||||
for ancestor in ancestors:
|
for ancestor in ancestors:
|
||||||
if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
|
old_name = frappe.db.get_value(
|
||||||
|
"Account",
|
||||||
|
{"account_number": old_acc_number, "account_name": old_acc_name, "company": ancestor},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
|
||||||
|
if old_name:
|
||||||
# same account in parent company exists
|
# same account in parent company exists
|
||||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plaid_success(token, response) {
|
plaid_success(token, response) {
|
||||||
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
|
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', {
|
||||||
|
response: response,
|
||||||
|
}).then(() => {
|
||||||
|
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
|
// Set default filter dates
|
||||||
|
today = frappe.datetime.get_today()
|
||||||
|
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
|
||||||
|
frm.doc.bank_statement_to_date = today;
|
||||||
frm.trigger('bank_account');
|
frm.trigger('bank_account');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -32,6 +36,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
|
frm.disable_save();
|
||||||
frappe.require("bank-reconciliation-tool.bundle.js", () =>
|
frappe.require("bank-reconciliation-tool.bundle.js", () =>
|
||||||
frm.trigger("make_reconciliation_tool")
|
frm.trigger("make_reconciliation_tool")
|
||||||
);
|
);
|
||||||
@@ -72,10 +77,12 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
after_save: function (frm) {
|
frm.add_custom_button(__('Get Unreconciled Entries'), function() {
|
||||||
frm.trigger("make_reconciliation_tool");
|
frm.trigger("make_reconciliation_tool");
|
||||||
|
});
|
||||||
|
frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
bank_account: function (frm) {
|
bank_account: function (frm) {
|
||||||
@@ -89,7 +96,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
r.account,
|
r.account,
|
||||||
"account_currency",
|
"account_currency",
|
||||||
(r) => {
|
(r) => {
|
||||||
frm.currency = r.account_currency;
|
frm.doc.account_currency = r.account_currency;
|
||||||
frm.trigger("render_chart");
|
frm.trigger("render_chart");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -155,19 +162,19 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render_chart: frappe.utils.debounce((frm) => {
|
render_chart(frm) {
|
||||||
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
|
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
|
||||||
{
|
{
|
||||||
$reconciliation_tool_cards: frm.get_field(
|
$reconciliation_tool_cards: frm.get_field(
|
||||||
"reconciliation_tool_cards"
|
"reconciliation_tool_cards"
|
||||||
).$wrapper,
|
).$wrapper,
|
||||||
bank_statement_closing_balance:
|
bank_statement_closing_balance:
|
||||||
frm.doc.bank_statement_closing_balance,
|
frm.doc.bank_statement_closing_balance,
|
||||||
cleared_balance: frm.cleared_balance,
|
cleared_balance: frm.cleared_balance,
|
||||||
currency: frm.currency,
|
currency: frm.doc.account_currency,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, 500),
|
},
|
||||||
|
|
||||||
render(frm) {
|
render(frm) {
|
||||||
if (frm.doc.bank_account) {
|
if (frm.doc.bank_account) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"to_reference_date",
|
"to_reference_date",
|
||||||
"filter_by_reference_date",
|
"filter_by_reference_date",
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
|
"account_currency",
|
||||||
"account_opening_balance",
|
"account_opening_balance",
|
||||||
"bank_statement_closing_balance",
|
"bank_statement_closing_balance",
|
||||||
"section_break_1",
|
"section_break_1",
|
||||||
@@ -59,7 +60,7 @@
|
|||||||
"fieldname": "account_opening_balance",
|
"fieldname": "account_opening_balance",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Account Opening Balance",
|
"label": "Account Opening Balance",
|
||||||
"options": "Currency",
|
"options": "account_currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
"fieldname": "bank_statement_closing_balance",
|
"fieldname": "bank_statement_closing_balance",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Closing Balance",
|
"label": "Closing Balance",
|
||||||
"options": "Currency"
|
"options": "account_currency"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_1",
|
"fieldname": "section_break_1",
|
||||||
@@ -104,13 +105,20 @@
|
|||||||
"fieldname": "filter_by_reference_date",
|
"fieldname": "filter_by_reference_date",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Filter by Reference Date"
|
"label": "Filter by Reference Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Account Currency",
|
||||||
|
"options": "Currency"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-01-13 13:00:02.022919",
|
"modified": "2023-03-07 11:02:24.535714",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Reconciliation Tool",
|
"name": "Bank Reconciliation Tool",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from frappe.model.document import Document
|
|||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
|
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||||
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
||||||
get_amounts_not_reflected_in_system,
|
get_amounts_not_reflected_in_system,
|
||||||
get_entries,
|
get_entries,
|
||||||
@@ -28,7 +28,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
|||||||
filters = []
|
filters = []
|
||||||
filters.append(["bank_account", "=", bank_account])
|
filters.append(["bank_account", "=", bank_account])
|
||||||
filters.append(["docstatus", "=", 1])
|
filters.append(["docstatus", "=", 1])
|
||||||
filters.append(["unallocated_amount", ">", 0])
|
filters.append(["unallocated_amount", ">", 0.0])
|
||||||
if to_date:
|
if to_date:
|
||||||
filters.append(["date", "<=", to_date])
|
filters.append(["date", "<=", to_date])
|
||||||
if from_date:
|
if from_date:
|
||||||
@@ -66,7 +66,7 @@ def get_account_balance(bank_account, till_date):
|
|||||||
|
|
||||||
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
||||||
|
|
||||||
total_debit, total_credit = 0, 0
|
total_debit, total_credit = 0.0, 0.0
|
||||||
for d in data:
|
for d in data:
|
||||||
total_debit += flt(d.debit)
|
total_debit += flt(d.debit)
|
||||||
total_credit += flt(d.credit)
|
total_credit += flt(d.credit)
|
||||||
@@ -145,10 +145,8 @@ def create_journal_entry_bts(
|
|||||||
accounts.append(
|
accounts.append(
|
||||||
{
|
{
|
||||||
"account": second_account,
|
"account": second_account,
|
||||||
"credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
|
"credit_in_account_currency": bank_transaction.deposit,
|
||||||
"debit_in_account_currency": bank_transaction.withdrawal
|
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||||
if bank_transaction.withdrawal > 0
|
|
||||||
else 0,
|
|
||||||
"party_type": party_type,
|
"party_type": party_type,
|
||||||
"party": party,
|
"party": party,
|
||||||
}
|
}
|
||||||
@@ -158,10 +156,8 @@ def create_journal_entry_bts(
|
|||||||
{
|
{
|
||||||
"account": company_account,
|
"account": company_account,
|
||||||
"bank_account": bank_transaction.bank_account,
|
"bank_account": bank_transaction.bank_account,
|
||||||
"credit_in_account_currency": bank_transaction.withdrawal
|
"credit_in_account_currency": bank_transaction.withdrawal,
|
||||||
if bank_transaction.withdrawal > 0
|
"debit_in_account_currency": bank_transaction.deposit,
|
||||||
else 0,
|
|
||||||
"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -185,16 +181,22 @@ def create_journal_entry_bts(
|
|||||||
journal_entry.insert()
|
journal_entry.insert()
|
||||||
journal_entry.submit()
|
journal_entry.submit()
|
||||||
|
|
||||||
if bank_transaction.deposit > 0:
|
if bank_transaction.deposit > 0.0:
|
||||||
paid_amount = bank_transaction.deposit
|
paid_amount = bank_transaction.deposit
|
||||||
else:
|
else:
|
||||||
paid_amount = bank_transaction.withdrawal
|
paid_amount = bank_transaction.withdrawal
|
||||||
|
|
||||||
vouchers = json.dumps(
|
vouchers = json.dumps(
|
||||||
[{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
|
[
|
||||||
|
{
|
||||||
|
"payment_doctype": "Journal Entry",
|
||||||
|
"payment_name": journal_entry.name,
|
||||||
|
"amount": paid_amount,
|
||||||
|
}
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return reconcile_vouchers(bank_transaction.name, vouchers)
|
return reconcile_vouchers(bank_transaction_name, vouchers)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -218,7 +220,7 @@ def create_payment_entry_bts(
|
|||||||
as_dict=True,
|
as_dict=True,
|
||||||
)[0]
|
)[0]
|
||||||
paid_amount = bank_transaction.unallocated_amount
|
paid_amount = bank_transaction.unallocated_amount
|
||||||
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
|
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
|
||||||
|
|
||||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||||
company = frappe.get_value("Account", company_account, "company")
|
company = frappe.get_value("Account", company_account, "company")
|
||||||
@@ -257,9 +259,15 @@ def create_payment_entry_bts(
|
|||||||
|
|
||||||
payment_entry.submit()
|
payment_entry.submit()
|
||||||
vouchers = json.dumps(
|
vouchers = json.dumps(
|
||||||
[{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
|
[
|
||||||
|
{
|
||||||
|
"payment_doctype": "Payment Entry",
|
||||||
|
"payment_name": payment_entry.name,
|
||||||
|
"amount": paid_amount,
|
||||||
|
}
|
||||||
|
]
|
||||||
)
|
)
|
||||||
return reconcile_vouchers(bank_transaction.name, vouchers)
|
return reconcile_vouchers(bank_transaction_name, vouchers)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -341,59 +349,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
|||||||
# updated clear date of all the vouchers based on the bank transaction
|
# updated clear date of all the vouchers based on the bank transaction
|
||||||
vouchers = json.loads(vouchers)
|
vouchers = json.loads(vouchers)
|
||||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||||
company_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
transaction.add_payment_entries(vouchers)
|
||||||
|
|
||||||
if transaction.unallocated_amount == 0:
|
|
||||||
frappe.throw(_("This bank transaction is already fully reconciled"))
|
|
||||||
total_amount = 0
|
|
||||||
for voucher in vouchers:
|
|
||||||
voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
|
|
||||||
total_amount += get_paid_amount(
|
|
||||||
frappe._dict(
|
|
||||||
{
|
|
||||||
"payment_document": voucher["payment_doctype"],
|
|
||||||
"payment_entry": voucher["payment_name"],
|
|
||||||
}
|
|
||||||
),
|
|
||||||
transaction.currency,
|
|
||||||
company_account,
|
|
||||||
)
|
|
||||||
|
|
||||||
if total_amount > transaction.unallocated_amount:
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
|
||||||
|
|
||||||
for voucher in vouchers:
|
|
||||||
gl_entry = frappe.db.get_value(
|
|
||||||
"GL Entry",
|
|
||||||
dict(
|
|
||||||
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
|
|
||||||
),
|
|
||||||
["credit_in_account_currency as credit", "debit_in_account_currency as debit"],
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
gl_amount, transaction_amount = (
|
|
||||||
(gl_entry.credit, transaction.deposit)
|
|
||||||
if gl_entry.credit > 0
|
|
||||||
else (gl_entry.debit, transaction.withdrawal)
|
|
||||||
)
|
|
||||||
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
|
|
||||||
|
|
||||||
transaction.append(
|
|
||||||
"payment_entries",
|
|
||||||
{
|
|
||||||
"payment_document": voucher["payment_entry"].doctype,
|
|
||||||
"payment_entry": voucher["payment_entry"].name,
|
|
||||||
"allocated_amount": allocated_amount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
transaction.save()
|
|
||||||
transaction.update_allocations()
|
|
||||||
return frappe.get_doc("Bank Transaction", bank_transaction_name)
|
return frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||||
|
|
||||||
|
|
||||||
@@ -412,9 +368,9 @@ def get_linked_payments(
|
|||||||
bank_account = frappe.db.get_values(
|
bank_account = frappe.db.get_values(
|
||||||
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
|
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
|
||||||
)[0]
|
)[0]
|
||||||
(account, company) = (bank_account.account, bank_account.company)
|
(gl_account, company) = (bank_account.account, bank_account.company)
|
||||||
matching = check_matching(
|
matching = check_matching(
|
||||||
account,
|
gl_account,
|
||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types,
|
||||||
@@ -424,7 +380,27 @@ def get_linked_payments(
|
|||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
)
|
)
|
||||||
return matching
|
return subtract_allocations(gl_account, matching)
|
||||||
|
|
||||||
|
|
||||||
|
def subtract_allocations(gl_account, vouchers):
|
||||||
|
"Look up & subtract any existing Bank Transaction allocations"
|
||||||
|
copied = []
|
||||||
|
for voucher in vouchers:
|
||||||
|
rows = get_total_allocated_amount(voucher[1], voucher[2])
|
||||||
|
amount = None
|
||||||
|
for row in rows:
|
||||||
|
if row["gl_account"] == gl_account:
|
||||||
|
amount = row["total"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if amount:
|
||||||
|
l = list(voucher)
|
||||||
|
l[3] -= amount
|
||||||
|
copied.append(tuple(l))
|
||||||
|
else:
|
||||||
|
copied.append(voucher)
|
||||||
|
return copied
|
||||||
|
|
||||||
|
|
||||||
def check_matching(
|
def check_matching(
|
||||||
@@ -438,6 +414,7 @@ def check_matching(
|
|||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
):
|
):
|
||||||
|
exact_match = True if "exact_match" in document_types else False
|
||||||
# combine all types of vouchers
|
# combine all types of vouchers
|
||||||
subquery = get_queries(
|
subquery = get_queries(
|
||||||
bank_account,
|
bank_account,
|
||||||
@@ -449,10 +426,11 @@ def check_matching(
|
|||||||
filter_by_reference_date,
|
filter_by_reference_date,
|
||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
|
exact_match,
|
||||||
)
|
)
|
||||||
filters = {
|
filters = {
|
||||||
"amount": transaction.unallocated_amount,
|
"amount": transaction.unallocated_amount,
|
||||||
"payment_type": "Receive" if transaction.deposit > 0 else "Pay",
|
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
|
||||||
"reference_no": transaction.reference_number,
|
"reference_no": transaction.reference_number,
|
||||||
"party_type": transaction.party_type,
|
"party_type": transaction.party_type,
|
||||||
"party": transaction.party,
|
"party": transaction.party,
|
||||||
@@ -461,7 +439,9 @@ def check_matching(
|
|||||||
|
|
||||||
matching_vouchers = []
|
matching_vouchers = []
|
||||||
|
|
||||||
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
|
matching_vouchers.extend(
|
||||||
|
get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match)
|
||||||
|
)
|
||||||
|
|
||||||
for query in subquery:
|
for query in subquery:
|
||||||
matching_vouchers.extend(
|
matching_vouchers.extend(
|
||||||
@@ -483,10 +463,10 @@ def get_queries(
|
|||||||
filter_by_reference_date,
|
filter_by_reference_date,
|
||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
|
exact_match,
|
||||||
):
|
):
|
||||||
# get queries to get matching vouchers
|
# get queries to get matching vouchers
|
||||||
amount_condition = "=" if "exact_match" in document_types else "<="
|
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||||
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
|
|
||||||
queries = []
|
queries = []
|
||||||
|
|
||||||
# get matching queries from all the apps
|
# get matching queries from all the apps
|
||||||
@@ -497,7 +477,7 @@ def get_queries(
|
|||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types,
|
||||||
amount_condition,
|
exact_match,
|
||||||
account_from_to,
|
account_from_to,
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
@@ -516,7 +496,7 @@ def get_matching_queries(
|
|||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
document_types,
|
document_types,
|
||||||
amount_condition,
|
exact_match,
|
||||||
account_from_to,
|
account_from_to,
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
@@ -526,8 +506,8 @@ def get_matching_queries(
|
|||||||
):
|
):
|
||||||
queries = []
|
queries = []
|
||||||
if "payment_entry" in document_types:
|
if "payment_entry" in document_types:
|
||||||
pe_amount_matching = get_pe_matching_query(
|
query = get_pe_matching_query(
|
||||||
amount_condition,
|
exact_match,
|
||||||
account_from_to,
|
account_from_to,
|
||||||
transaction,
|
transaction,
|
||||||
from_date,
|
from_date,
|
||||||
@@ -536,11 +516,11 @@ def get_matching_queries(
|
|||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
)
|
)
|
||||||
queries.extend([pe_amount_matching])
|
queries.append(query)
|
||||||
|
|
||||||
if "journal_entry" in document_types:
|
if "journal_entry" in document_types:
|
||||||
je_amount_matching = get_je_matching_query(
|
query = get_je_matching_query(
|
||||||
amount_condition,
|
exact_match,
|
||||||
transaction,
|
transaction,
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
@@ -548,34 +528,70 @@ def get_matching_queries(
|
|||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
)
|
)
|
||||||
queries.extend([je_amount_matching])
|
queries.append(query)
|
||||||
|
|
||||||
if transaction.deposit > 0 and "sales_invoice" in document_types:
|
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||||
si_amount_matching = get_si_matching_query(amount_condition)
|
query = get_si_matching_query(exact_match)
|
||||||
queries.extend([si_amount_matching])
|
queries.append(query)
|
||||||
|
|
||||||
if transaction.withdrawal > 0:
|
if transaction.withdrawal > 0.0:
|
||||||
if "purchase_invoice" in document_types:
|
if "purchase_invoice" in document_types:
|
||||||
pi_amount_matching = get_pi_matching_query(amount_condition)
|
query = get_pi_matching_query(exact_match)
|
||||||
queries.extend([pi_amount_matching])
|
queries.append(query)
|
||||||
|
|
||||||
|
if "bank_transaction" in document_types:
|
||||||
|
query = get_bt_matching_query(exact_match, transaction)
|
||||||
|
queries.append(query)
|
||||||
|
|
||||||
return queries
|
return queries
|
||||||
|
|
||||||
|
|
||||||
def get_loan_vouchers(bank_account, transaction, document_types, filters):
|
def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match):
|
||||||
vouchers = []
|
vouchers = []
|
||||||
amount_condition = True if "exact_match" in document_types else False
|
|
||||||
|
|
||||||
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
|
if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
|
||||||
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
|
vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
|
||||||
|
|
||||||
if transaction.deposit > 0 and "loan_repayment" in document_types:
|
if transaction.deposit > 0.0 and "loan_repayment" in document_types:
|
||||||
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
|
vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
|
||||||
|
|
||||||
return vouchers
|
return vouchers
|
||||||
|
|
||||||
|
|
||||||
def get_ld_matching_query(bank_account, amount_condition, filters):
|
def get_bt_matching_query(exact_match, transaction):
|
||||||
|
# get matching bank transaction query
|
||||||
|
# find bank transactions in the same bank account with opposite sign
|
||||||
|
# same bank account must have same company and currency
|
||||||
|
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
(CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
|
||||||
|
+ CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
|
||||||
|
+ CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
|
||||||
|
+ CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
|
||||||
|
+ 1) AS rank,
|
||||||
|
'Bank Transaction' AS doctype,
|
||||||
|
name,
|
||||||
|
unallocated_amount AS paid_amount,
|
||||||
|
reference_number AS reference_no,
|
||||||
|
date AS reference_date,
|
||||||
|
party,
|
||||||
|
party_type,
|
||||||
|
date AS posting_date,
|
||||||
|
currency
|
||||||
|
FROM
|
||||||
|
`tabBank Transaction`
|
||||||
|
WHERE
|
||||||
|
status != 'Reconciled'
|
||||||
|
AND name != '{transaction.name}'
|
||||||
|
AND bank_account = '{transaction.bank_account}'
|
||||||
|
AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_ld_matching_query(bank_account, exact_match, filters):
|
||||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||||
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
|
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
|
||||||
matching_party = loan_disbursement.applicant_type == filters.get(
|
matching_party = loan_disbursement.applicant_type == filters.get(
|
||||||
@@ -603,17 +619,17 @@ def get_ld_matching_query(bank_account, amount_condition, filters):
|
|||||||
.where(loan_disbursement.disbursement_account == bank_account)
|
.where(loan_disbursement.disbursement_account == bank_account)
|
||||||
)
|
)
|
||||||
|
|
||||||
if amount_condition:
|
if exact_match:
|
||||||
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
|
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
|
||||||
else:
|
else:
|
||||||
query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
|
query.where(loan_disbursement.disbursed_amount > 0.0)
|
||||||
|
|
||||||
vouchers = query.run(as_list=True)
|
vouchers = query.run(as_list=True)
|
||||||
|
|
||||||
return vouchers
|
return vouchers
|
||||||
|
|
||||||
|
|
||||||
def get_lr_matching_query(bank_account, amount_condition, filters):
|
def get_lr_matching_query(bank_account, exact_match, filters):
|
||||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||||
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
|
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
|
||||||
matching_party = loan_repayment.applicant_type == filters.get(
|
matching_party = loan_repayment.applicant_type == filters.get(
|
||||||
@@ -644,10 +660,10 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
|
|||||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||||
|
|
||||||
if amount_condition:
|
if exact_match:
|
||||||
query.where(loan_repayment.amount_paid == filters.get("amount"))
|
query.where(loan_repayment.amount_paid == filters.get("amount"))
|
||||||
else:
|
else:
|
||||||
query.where(loan_repayment.amount_paid <= filters.get("amount"))
|
query.where(loan_repayment.amount_paid > 0.0)
|
||||||
|
|
||||||
vouchers = query.run()
|
vouchers = query.run()
|
||||||
|
|
||||||
@@ -655,7 +671,7 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_pe_matching_query(
|
def get_pe_matching_query(
|
||||||
amount_condition,
|
exact_match,
|
||||||
account_from_to,
|
account_from_to,
|
||||||
transaction,
|
transaction,
|
||||||
from_date,
|
from_date,
|
||||||
@@ -665,7 +681,7 @@ def get_pe_matching_query(
|
|||||||
to_reference_date,
|
to_reference_date,
|
||||||
):
|
):
|
||||||
# get matching payment entries query
|
# get matching payment entries query
|
||||||
if transaction.deposit > 0:
|
if transaction.deposit > 0.0:
|
||||||
currency_field = "paid_to_account_currency as currency"
|
currency_field = "paid_to_account_currency as currency"
|
||||||
else:
|
else:
|
||||||
currency_field = "paid_from_account_currency as currency"
|
currency_field = "paid_from_account_currency as currency"
|
||||||
@@ -680,7 +696,8 @@ def get_pe_matching_query(
|
|||||||
return f"""
|
return f"""
|
||||||
SELECT
|
SELECT
|
||||||
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
|
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
|
||||||
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
|
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
|
||||||
|
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
|
||||||
+ 1 ) AS rank,
|
+ 1 ) AS rank,
|
||||||
'Payment Entry' as doctype,
|
'Payment Entry' as doctype,
|
||||||
name,
|
name,
|
||||||
@@ -694,20 +711,19 @@ def get_pe_matching_query(
|
|||||||
FROM
|
FROM
|
||||||
`tabPayment Entry`
|
`tabPayment Entry`
|
||||||
WHERE
|
WHERE
|
||||||
paid_amount {amount_condition} %(amount)s
|
docstatus = 1
|
||||||
AND docstatus = 1
|
|
||||||
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
|
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
|
||||||
AND ifnull(clearance_date, '') = ""
|
AND ifnull(clearance_date, '') = ""
|
||||||
AND {account_from_to} = %(bank_account)s
|
AND {account_from_to} = %(bank_account)s
|
||||||
|
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||||
{filter_by_date}
|
{filter_by_date}
|
||||||
{filter_by_reference_no}
|
{filter_by_reference_no}
|
||||||
order by{order_by}
|
order by{order_by}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_je_matching_query(
|
def get_je_matching_query(
|
||||||
amount_condition,
|
exact_match,
|
||||||
transaction,
|
transaction,
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
@@ -719,7 +735,7 @@ def get_je_matching_query(
|
|||||||
# We have mapping at the bank level
|
# We have mapping at the bank level
|
||||||
# So one bank could have both types of bank accounts like asset and liability
|
# So one bank could have both types of bank accounts like asset and liability
|
||||||
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
|
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
|
||||||
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
|
cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit"
|
||||||
filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
|
filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
|
||||||
order_by = " je.posting_date"
|
order_by = " je.posting_date"
|
||||||
filter_by_reference_no = ""
|
filter_by_reference_no = ""
|
||||||
@@ -731,26 +747,29 @@ def get_je_matching_query(
|
|||||||
return f"""
|
return f"""
|
||||||
SELECT
|
SELECT
|
||||||
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
|
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
|
||||||
|
+ CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
|
||||||
+ 1) AS rank ,
|
+ 1) AS rank ,
|
||||||
'Journal Entry' as doctype,
|
'Journal Entry' AS doctype,
|
||||||
je.name,
|
je.name,
|
||||||
jea.{cr_or_dr}_in_account_currency as paid_amount,
|
jea.{cr_or_dr}_in_account_currency AS paid_amount,
|
||||||
je.cheque_no as reference_no,
|
je.cheque_no AS reference_no,
|
||||||
je.cheque_date as reference_date,
|
je.cheque_date AS reference_date,
|
||||||
je.pay_to_recd_from as party,
|
je.pay_to_recd_from AS party,
|
||||||
jea.party_type,
|
jea.party_type,
|
||||||
je.posting_date,
|
je.posting_date,
|
||||||
jea.account_currency as currency
|
jea.account_currency AS currency
|
||||||
FROM
|
FROM
|
||||||
`tabJournal Entry Account` as jea
|
`tabJournal Entry Account` AS jea
|
||||||
JOIN
|
JOIN
|
||||||
`tabJournal Entry` as je
|
`tabJournal Entry` AS je
|
||||||
ON
|
ON
|
||||||
jea.parent = je.name
|
jea.parent = je.name
|
||||||
WHERE
|
WHERE
|
||||||
(je.clearance_date is null or je.clearance_date='0000-00-00')
|
je.docstatus = 1
|
||||||
|
AND je.voucher_type NOT IN ('Opening Entry')
|
||||||
|
AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
|
||||||
AND jea.account = %(bank_account)s
|
AND jea.account = %(bank_account)s
|
||||||
AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
|
AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
|
||||||
AND je.docstatus = 1
|
AND je.docstatus = 1
|
||||||
{filter_by_date}
|
{filter_by_date}
|
||||||
{filter_by_reference_no}
|
{filter_by_reference_no}
|
||||||
@@ -758,11 +777,12 @@ def get_je_matching_query(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_si_matching_query(amount_condition):
|
def get_si_matching_query(exact_match):
|
||||||
# get matchin sales invoice query
|
# get matching sales invoice query
|
||||||
return f"""
|
return f"""
|
||||||
SELECT
|
SELECT
|
||||||
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
|
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
|
||||||
|
+ CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
|
||||||
+ 1 ) AS rank,
|
+ 1 ) AS rank,
|
||||||
'Sales Invoice' as doctype,
|
'Sales Invoice' as doctype,
|
||||||
si.name,
|
si.name,
|
||||||
@@ -780,18 +800,20 @@ def get_si_matching_query(amount_condition):
|
|||||||
`tabSales Invoice` as si
|
`tabSales Invoice` as si
|
||||||
ON
|
ON
|
||||||
sip.parent = si.name
|
sip.parent = si.name
|
||||||
WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
|
WHERE
|
||||||
|
si.docstatus = 1
|
||||||
|
AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
|
||||||
AND sip.account = %(bank_account)s
|
AND sip.account = %(bank_account)s
|
||||||
AND sip.amount {amount_condition} %(amount)s
|
AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||||
AND si.docstatus = 1
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_pi_matching_query(amount_condition):
|
def get_pi_matching_query(exact_match):
|
||||||
# get matching purchase invoice query
|
# get matching purchase invoice query when they are also used as payment entries (is_paid)
|
||||||
return f"""
|
return f"""
|
||||||
SELECT
|
SELECT
|
||||||
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
|
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
|
||||||
|
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
|
||||||
+ 1 ) AS rank,
|
+ 1 ) AS rank,
|
||||||
'Purchase Invoice' as doctype,
|
'Purchase Invoice' as doctype,
|
||||||
name,
|
name,
|
||||||
@@ -805,9 +827,9 @@ def get_pi_matching_query(amount_condition):
|
|||||||
FROM
|
FROM
|
||||||
`tabPurchase Invoice`
|
`tabPurchase Invoice`
|
||||||
WHERE
|
WHERE
|
||||||
paid_amount {amount_condition} %(amount)s
|
docstatus = 1
|
||||||
AND docstatus = 1
|
|
||||||
AND is_paid = 1
|
AND is_paid = 1
|
||||||
AND ifnull(clearance_date, '') = ""
|
AND ifnull(clearance_date, '') = ""
|
||||||
AND cash_bank_account = %(bank_account)s
|
AND cash_bank_account = %(bank_account)s
|
||||||
|
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -12,8 +12,13 @@ frappe.ui.form.on("Bank Transaction", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
refresh(frm) {
|
||||||
bank_account: function(frm) {
|
frm.add_custom_button(__('Unreconcile Transaction'), () => {
|
||||||
|
frm.call('remove_payment_entries')
|
||||||
|
.then( () => frm.refresh() );
|
||||||
|
});
|
||||||
|
},
|
||||||
|
bank_account: function (frm) {
|
||||||
set_bank_statement_filter(frm);
|
set_bank_statement_filter(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -34,6 +39,7 @@ frappe.ui.form.on("Bank Transaction", {
|
|||||||
"Journal Entry",
|
"Journal Entry",
|
||||||
"Sales Invoice",
|
"Sales Invoice",
|
||||||
"Purchase Invoice",
|
"Purchase Invoice",
|
||||||
|
"Bank Transaction",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -49,7 +55,7 @@ const update_clearance_date = (frm, cdt, cdn) => {
|
|||||||
frappe
|
frappe
|
||||||
.xcall(
|
.xcall(
|
||||||
"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
|
"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
|
||||||
{ doctype: cdt, docname: cdn }
|
{ doctype: cdt, docname: cdn, bt_name: frm.doc.name }
|
||||||
)
|
)
|
||||||
.then((e) => {
|
.then((e) => {
|
||||||
if (e == "success") {
|
if (e == "success") {
|
||||||
|
|||||||
@@ -20,9 +20,11 @@
|
|||||||
"currency",
|
"currency",
|
||||||
"section_break_10",
|
"section_break_10",
|
||||||
"description",
|
"description",
|
||||||
"section_break_14",
|
|
||||||
"reference_number",
|
"reference_number",
|
||||||
|
"column_break_10",
|
||||||
"transaction_id",
|
"transaction_id",
|
||||||
|
"transaction_type",
|
||||||
|
"section_break_14",
|
||||||
"payment_entries",
|
"payment_entries",
|
||||||
"section_break_18",
|
"section_break_18",
|
||||||
"allocated_amount",
|
"allocated_amount",
|
||||||
@@ -190,11 +192,21 @@
|
|||||||
"label": "Withdrawal",
|
"label": "Withdrawal",
|
||||||
"oldfieldname": "credit",
|
"oldfieldname": "credit",
|
||||||
"options": "currency"
|
"options": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_10",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "transaction_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Transaction Type",
|
||||||
|
"length": 50
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-03-21 19:05:04.208222",
|
"modified": "2022-05-29 18:36:50.475964",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Transaction",
|
"name": "Bank Transaction",
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
@@ -18,72 +15,137 @@ class BankTransaction(StatusUpdater):
|
|||||||
self.clear_linked_payment_entries()
|
self.clear_linked_payment_entries()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
|
_saving_flag = False
|
||||||
|
|
||||||
|
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
self.update_allocations()
|
"Run on save(). Avoid recursion caused by multiple saves"
|
||||||
self.clear_linked_payment_entries()
|
if not self._saving_flag:
|
||||||
self.set_status(update=True)
|
self._saving_flag = True
|
||||||
|
self.clear_linked_payment_entries()
|
||||||
|
self.update_allocations()
|
||||||
|
self._saving_flag = False
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.clear_linked_payment_entries(for_cancel=True)
|
self.clear_linked_payment_entries(for_cancel=True)
|
||||||
self.set_status(update=True)
|
self.set_status(update=True)
|
||||||
|
|
||||||
def update_allocations(self):
|
def update_allocations(self):
|
||||||
|
"The doctype does not allow modifications after submission, so write to the db direct"
|
||||||
if self.payment_entries:
|
if self.payment_entries:
|
||||||
allocated_amount = reduce(
|
allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
|
||||||
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
allocated_amount = 0
|
allocated_amount = 0.0
|
||||||
|
|
||||||
if allocated_amount:
|
amount = abs(flt(self.withdrawal) - flt(self.deposit))
|
||||||
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
|
self.db_set("allocated_amount", flt(allocated_amount))
|
||||||
frappe.db.set_value(
|
self.db_set("unallocated_amount", amount - flt(allocated_amount))
|
||||||
self.doctype,
|
self.reload()
|
||||||
self.name,
|
self.set_status(update=True)
|
||||||
"unallocated_amount",
|
|
||||||
abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
def add_payment_entries(self, vouchers):
|
||||||
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
|
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
|
||||||
frappe.db.set_value(
|
if 0.0 >= self.unallocated_amount:
|
||||||
self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
|
frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled"))
|
||||||
)
|
|
||||||
|
|
||||||
amount = self.deposit or self.withdrawal
|
added = False
|
||||||
if amount == self.allocated_amount:
|
for voucher in vouchers:
|
||||||
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
|
# Can't add same voucher twice
|
||||||
|
found = False
|
||||||
|
for pe in self.payment_entries:
|
||||||
|
if (
|
||||||
|
pe.payment_document == voucher["payment_doctype"]
|
||||||
|
and pe.payment_entry == voucher["payment_name"]
|
||||||
|
):
|
||||||
|
found = True
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
pe = {
|
||||||
|
"payment_document": voucher["payment_doctype"],
|
||||||
|
"payment_entry": voucher["payment_name"],
|
||||||
|
"allocated_amount": 0.0, # Temporary
|
||||||
|
}
|
||||||
|
child = self.append("payment_entries", pe)
|
||||||
|
added = True
|
||||||
|
|
||||||
|
# runs on_update_after_submit
|
||||||
|
if added:
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def allocate_payment_entries(self):
|
||||||
|
"""Refactored from bank reconciliation tool.
|
||||||
|
Non-zero allocations must be amended/cleared manually
|
||||||
|
Get the bank transaction amount (b) and remove as we allocate
|
||||||
|
For each payment_entry if allocated_amount == 0:
|
||||||
|
- get the amount already allocated against all transactions (t), need latest date
|
||||||
|
- get the voucher amount (from gl) (v)
|
||||||
|
- allocate (a = v - t)
|
||||||
|
- a = 0: should already be cleared, so clear & remove payment_entry
|
||||||
|
- 0 < a <= u: allocate a & clear
|
||||||
|
- 0 < a, a > u: allocate u
|
||||||
|
- 0 > a: Error: already over-allocated
|
||||||
|
- clear means: set the latest transaction date as clearance date
|
||||||
|
"""
|
||||||
|
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
|
||||||
|
remaining_amount = self.unallocated_amount
|
||||||
|
for payment_entry in self.payment_entries:
|
||||||
|
if payment_entry.allocated_amount == 0.0:
|
||||||
|
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||||
|
self, payment_entry
|
||||||
|
)
|
||||||
|
|
||||||
|
if 0.0 == unallocated_amount:
|
||||||
|
if should_clear:
|
||||||
|
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||||
|
self.db_delete_payment_entry(payment_entry)
|
||||||
|
|
||||||
|
elif remaining_amount <= 0.0:
|
||||||
|
self.db_delete_payment_entry(payment_entry)
|
||||||
|
|
||||||
|
elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
|
||||||
|
payment_entry.db_set("allocated_amount", unallocated_amount)
|
||||||
|
remaining_amount -= unallocated_amount
|
||||||
|
if should_clear:
|
||||||
|
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||||
|
|
||||||
|
elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
|
||||||
|
payment_entry.db_set("allocated_amount", remaining_amount)
|
||||||
|
remaining_amount = 0.0
|
||||||
|
|
||||||
|
elif 0.0 > unallocated_amount:
|
||||||
|
self.db_delete_payment_entry(payment_entry)
|
||||||
|
frappe.throw(
|
||||||
|
frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
|
||||||
|
)
|
||||||
|
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
def clear_linked_payment_entries(self, for_cancel=False):
|
def db_delete_payment_entry(self, payment_entry):
|
||||||
|
frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def remove_payment_entries(self):
|
||||||
for payment_entry in self.payment_entries:
|
for payment_entry in self.payment_entries:
|
||||||
if payment_entry.payment_document == "Sales Invoice":
|
self.remove_payment_entry(payment_entry)
|
||||||
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
|
# runs on_update_after_submit
|
||||||
elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
|
self.save()
|
||||||
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
|
|
||||||
|
|
||||||
def clear_simple_entry(self, payment_entry, for_cancel=False):
|
def remove_payment_entry(self, payment_entry):
|
||||||
if payment_entry.payment_document == "Payment Entry":
|
"Clear payment entry and clearance"
|
||||||
if (
|
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||||
frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
|
self.remove(payment_entry)
|
||||||
== "Internal Transfer"
|
|
||||||
):
|
|
||||||
if len(get_reconciled_bank_transactions(payment_entry)) < 2:
|
|
||||||
return
|
|
||||||
|
|
||||||
clearance_date = self.date if not for_cancel else None
|
def clear_linked_payment_entries(self, for_cancel=False):
|
||||||
frappe.db.set_value(
|
if for_cancel:
|
||||||
payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
|
for payment_entry in self.payment_entries:
|
||||||
)
|
self.clear_linked_payment_entry(payment_entry, for_cancel)
|
||||||
|
else:
|
||||||
|
self.allocate_payment_entries()
|
||||||
|
|
||||||
def clear_sales_invoice(self, payment_entry, for_cancel=False):
|
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
|
||||||
clearance_date = self.date if not for_cancel else None
|
clearance_date = None if for_cancel else self.date
|
||||||
frappe.db.set_value(
|
set_voucher_clearance(
|
||||||
"Sales Invoice Payment",
|
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
|
||||||
dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
|
|
||||||
"clearance_date",
|
|
||||||
clearance_date,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -93,38 +155,112 @@ def get_doctypes_for_bank_reconciliation():
|
|||||||
return frappe.get_hooks("bank_reconciliation_doctypes")
|
return frappe.get_hooks("bank_reconciliation_doctypes")
|
||||||
|
|
||||||
|
|
||||||
def get_reconciled_bank_transactions(payment_entry):
|
def get_clearance_details(transaction, payment_entry):
|
||||||
reconciled_bank_transactions = frappe.get_all(
|
"""
|
||||||
"Bank Transaction Payments",
|
There should only be one bank gle for a voucher.
|
||||||
filters={"payment_entry": payment_entry.payment_entry},
|
Could be none for a Bank Transaction.
|
||||||
fields=["parent"],
|
But if a JE, could affect two banks.
|
||||||
|
Should only clear the voucher if all bank gles are allocated.
|
||||||
|
"""
|
||||||
|
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||||
|
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||||
|
bt_allocations = get_total_allocated_amount(
|
||||||
|
payment_entry.payment_document, payment_entry.payment_entry
|
||||||
)
|
)
|
||||||
|
|
||||||
return reconciled_bank_transactions
|
unallocated_amount = min(
|
||||||
|
transaction.unallocated_amount,
|
||||||
|
get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
|
||||||
|
)
|
||||||
|
unmatched_gles = len(gles)
|
||||||
|
latest_transaction = transaction
|
||||||
|
for gle in gles:
|
||||||
|
if gle["gl_account"] == gl_bank_account:
|
||||||
|
if gle["amount"] <= 0.0:
|
||||||
|
frappe.throw(
|
||||||
|
frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}")
|
||||||
|
)
|
||||||
|
|
||||||
|
unmatched_gles -= 1
|
||||||
|
unallocated_amount = gle["amount"]
|
||||||
|
for a in bt_allocations:
|
||||||
|
if a["gl_account"] == gle["gl_account"]:
|
||||||
|
unallocated_amount = gle["amount"] - a["total"]
|
||||||
|
if frappe.utils.getdate(transaction.date) < a["latest_date"]:
|
||||||
|
latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"])
|
||||||
|
else:
|
||||||
|
# Must be a Journal Entry affecting more than one bank
|
||||||
|
for a in bt_allocations:
|
||||||
|
if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]:
|
||||||
|
unmatched_gles -= 1
|
||||||
|
|
||||||
|
return unallocated_amount, unmatched_gles == 0, latest_transaction
|
||||||
|
|
||||||
|
|
||||||
def get_total_allocated_amount(payment_entry):
|
def get_related_bank_gl_entries(doctype, docname):
|
||||||
return frappe.db.sql(
|
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||||
|
result = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
SUM(btp.allocated_amount) as allocated_amount,
|
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
|
||||||
bt.name
|
gle.account AS gl_account
|
||||||
FROM
|
FROM
|
||||||
`tabBank Transaction Payments` as btp
|
`tabGL Entry` gle
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
`tabBank Transaction` bt ON bt.name=btp.parent
|
`tabAccount` ac ON ac.name=gle.account
|
||||||
WHERE
|
WHERE
|
||||||
btp.payment_document = %s
|
ac.account_type = 'Bank'
|
||||||
AND
|
AND gle.voucher_type = %(doctype)s
|
||||||
btp.payment_entry = %s
|
AND gle.voucher_no = %(docname)s
|
||||||
AND
|
AND is_cancelled = 0
|
||||||
bt.docstatus = 1""",
|
""",
|
||||||
(payment_entry.payment_document, payment_entry.payment_entry),
|
dict(doctype=doctype, docname=docname),
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_paid_amount(payment_entry, currency, bank_account):
|
def get_total_allocated_amount(doctype, docname):
|
||||||
|
"""
|
||||||
|
Gets the sum of allocations for a voucher on each bank GL account
|
||||||
|
along with the latest bank transaction name & date
|
||||||
|
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||||
|
"""
|
||||||
|
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||||
|
result = frappe.db.sql(
|
||||||
|
"""
|
||||||
|
SELECT total, latest_name, latest_date, gl_account FROM (
|
||||||
|
SELECT
|
||||||
|
ROW_NUMBER() OVER w AS rownum,
|
||||||
|
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
|
||||||
|
FIRST_VALUE(bt.name) OVER w AS latest_name,
|
||||||
|
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||||
|
ba.account AS gl_account
|
||||||
|
FROM
|
||||||
|
`tabBank Transaction Payments` btp
|
||||||
|
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
|
||||||
|
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
|
||||||
|
WHERE
|
||||||
|
btp.payment_document = %(doctype)s
|
||||||
|
AND btp.payment_entry = %(docname)s
|
||||||
|
AND bt.docstatus = 1
|
||||||
|
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
|
||||||
|
) temp
|
||||||
|
WHERE
|
||||||
|
rownum = 1
|
||||||
|
""",
|
||||||
|
dict(doctype=doctype, docname=docname),
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
for row in result:
|
||||||
|
# Why is this *sometimes* a byte string?
|
||||||
|
if isinstance(row["latest_name"], bytes):
|
||||||
|
row["latest_name"] = row["latest_name"].decode()
|
||||||
|
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||||
|
|
||||||
paid_amount_field = "paid_amount"
|
paid_amount_field = "paid_amount"
|
||||||
@@ -147,7 +283,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
|||||||
elif payment_entry.payment_document == "Journal Entry":
|
elif payment_entry.payment_document == "Journal Entry":
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Journal Entry Account",
|
"Journal Entry Account",
|
||||||
{"parent": payment_entry.payment_entry, "account": bank_account},
|
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
|
||||||
"sum(credit_in_account_currency)",
|
"sum(credit_in_account_currency)",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -166,6 +302,12 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
|||||||
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
|
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif payment_entry.payment_document == "Bank Transaction":
|
||||||
|
dep, wth = frappe.db.get_value(
|
||||||
|
"Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal")
|
||||||
|
)
|
||||||
|
return abs(flt(wth) - flt(dep))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
"Please reconcile {0}: {1} manually".format(
|
"Please reconcile {0}: {1} manually".format(
|
||||||
@@ -174,18 +316,55 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
def set_voucher_clearance(doctype, docname, clearance_date, self):
|
||||||
def unclear_reference_payment(doctype, docname):
|
if doctype in [
|
||||||
if frappe.db.exists(doctype, docname):
|
"Payment Entry",
|
||||||
doc = frappe.get_doc(doctype, docname)
|
"Journal Entry",
|
||||||
if doctype == "Sales Invoice":
|
"Purchase Invoice",
|
||||||
frappe.db.set_value(
|
"Expense Claim",
|
||||||
"Sales Invoice Payment",
|
"Loan Repayment",
|
||||||
dict(parenttype=doc.payment_document, parent=doc.payment_entry),
|
"Loan Disbursement",
|
||||||
"clearance_date",
|
]:
|
||||||
None,
|
if (
|
||||||
)
|
doctype == "Payment Entry"
|
||||||
else:
|
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
|
||||||
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
|
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
|
||||||
|
):
|
||||||
|
return
|
||||||
|
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||||
|
|
||||||
return doc.payment_entry
|
elif doctype == "Sales Invoice":
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Sales Invoice Payment",
|
||||||
|
dict(parenttype=doctype, parent=docname),
|
||||||
|
"clearance_date",
|
||||||
|
clearance_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif doctype == "Bank Transaction":
|
||||||
|
# For when a second bank transaction has fixed another, e.g. refund
|
||||||
|
bt = frappe.get_doc(doctype, docname)
|
||||||
|
if clearance_date:
|
||||||
|
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
|
||||||
|
bt.add_payment_entries(vouchers)
|
||||||
|
else:
|
||||||
|
for pe in bt.payment_entries:
|
||||||
|
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
|
||||||
|
bt.remove(pe)
|
||||||
|
bt.save()
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def get_reconciled_bank_transactions(doctype, docname):
|
||||||
|
return frappe.get_all(
|
||||||
|
"Bank Transaction Payments",
|
||||||
|
filters={"payment_document": doctype, "payment_entry": docname},
|
||||||
|
pluck="parent",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def unclear_reference_payment(doctype, docname, bt_name):
|
||||||
|
bt = frappe.get_doc("Bank Transaction", bt_name)
|
||||||
|
set_voucher_clearance(doctype, docname, None, bt)
|
||||||
|
return docname
|
||||||
|
|||||||
@@ -221,12 +221,15 @@ class PaymentReconciliation(Document):
|
|||||||
|
|
||||||
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||||
difference_amount = 0
|
difference_amount = 0
|
||||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
if frappe.get_cached_value(
|
||||||
"exchange_rate", 1
|
"Account", self.receivable_payable_account, "account_currency"
|
||||||
):
|
) != frappe.get_cached_value("Company", self.company, "default_currency"):
|
||||||
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
|
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||||
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
|
"exchange_rate", 1
|
||||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
):
|
||||||
|
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
|
||||||
|
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
|
||||||
|
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||||
|
|
||||||
return difference_amount
|
return difference_amount
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, flt, nowdate
|
from frappe.utils import add_days, flt, nowdate
|
||||||
|
|
||||||
from erpnext import get_default_cost_center
|
from erpnext import get_default_cost_center
|
||||||
@@ -349,6 +349,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||||
payments = [x.as_dict() for x in pr.get("payments")]
|
payments = [x.as_dict() for x in pr.get("payments")]
|
||||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
|
||||||
|
# Difference amount should not be calculated for base currency accounts
|
||||||
|
for row in pr.allocation:
|
||||||
|
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||||
|
|
||||||
pr.reconcile()
|
pr.reconcile()
|
||||||
|
|
||||||
si.reload()
|
si.reload()
|
||||||
@@ -390,6 +395,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||||
payments = [x.as_dict() for x in pr.get("payments")]
|
payments = [x.as_dict() for x in pr.get("payments")]
|
||||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
|
||||||
|
# Difference amount should not be calculated for base currency accounts
|
||||||
|
for row in pr.allocation:
|
||||||
|
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||||
|
|
||||||
pr.reconcile()
|
pr.reconcile()
|
||||||
|
|
||||||
# check PR tool output
|
# check PR tool output
|
||||||
@@ -414,6 +424,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||||
payments = [x.as_dict() for x in pr.get("payments")]
|
payments = [x.as_dict() for x in pr.get("payments")]
|
||||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
|
||||||
|
# Difference amount should not be calculated for base currency accounts
|
||||||
|
for row in pr.allocation:
|
||||||
|
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||||
|
|
||||||
pr.reconcile()
|
pr.reconcile()
|
||||||
|
|
||||||
# assert outstanding
|
# assert outstanding
|
||||||
@@ -450,6 +465,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||||
payments = [x.as_dict() for x in pr.get("payments")]
|
payments = [x.as_dict() for x in pr.get("payments")]
|
||||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
|
||||||
|
# Difference amount should not be calculated for base currency accounts
|
||||||
|
for row in pr.allocation:
|
||||||
|
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||||
|
|
||||||
pr.reconcile()
|
pr.reconcile()
|
||||||
|
|
||||||
self.assertEqual(pr.get("invoices"), [])
|
self.assertEqual(pr.get("invoices"), [])
|
||||||
@@ -824,6 +844,52 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
|
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
|
||||||
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
|
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{
|
||||||
|
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_no_difference_amount_for_base_currency_accounts(self):
|
||||||
|
# Make Sale Invoice
|
||||||
|
si = self.create_sales_invoice(
|
||||||
|
qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||||
|
)
|
||||||
|
si.customer = self.customer
|
||||||
|
si.currency = "EUR"
|
||||||
|
si.conversion_rate = 85
|
||||||
|
si.debit_to = self.debit_to
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
# Make payment using Payment Entry
|
||||||
|
pe1 = create_payment_entry(
|
||||||
|
company=self.company,
|
||||||
|
payment_type="Receive",
|
||||||
|
party_type="Customer",
|
||||||
|
party=self.customer,
|
||||||
|
paid_from=self.debit_to,
|
||||||
|
paid_to=self.bank,
|
||||||
|
paid_amount=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
pe1.save()
|
||||||
|
pe1.submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
pr.party = self.customer
|
||||||
|
pr.receivable_payable_account = self.debit_to
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [pr.payments[0].as_dict()]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
|
||||||
|
self.assertEqual(pr.allocation[0].allocated_amount, 85)
|
||||||
|
self.assertEqual(pr.allocation[0].difference_amount, 0)
|
||||||
|
|
||||||
|
|
||||||
def make_customer(customer_name, currency=None):
|
def make_customer(customer_name, currency=None):
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
|
|||||||
@@ -112,7 +112,8 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
|||||||
party_type: "Customer",
|
party_type: "Customer",
|
||||||
account: this.frm.doc.debit_to,
|
account: this.frm.doc.debit_to,
|
||||||
price_list: this.frm.doc.selling_price_list,
|
price_list: this.frm.doc.selling_price_list,
|
||||||
pos_profile: pos_profile
|
pos_profile: pos_profile,
|
||||||
|
company_address: this.frm.doc.company_address
|
||||||
}, () => {
|
}, () => {
|
||||||
this.apply_pricing_rule();
|
this.apply_pricing_rule();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -859,7 +859,7 @@ class ReceivablePayableReport(object):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.qb_selection_filter.append(
|
self.qb_selection_filter.append(
|
||||||
self.ple[dimension.fieldname] == self.filters[dimension.fieldname]
|
self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname])
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_invoice(self, ple):
|
def is_invoice(self, ple):
|
||||||
|
|||||||
@@ -501,7 +501,14 @@ class GrossProfitGenerator(object):
|
|||||||
):
|
):
|
||||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||||
for returned_item_row in returned_item_rows:
|
for returned_item_row in returned_item_rows:
|
||||||
row.qty += flt(returned_item_row.qty)
|
# returned_items 'qty' should be stateful
|
||||||
|
if returned_item_row.qty != 0:
|
||||||
|
if row.qty >= abs(returned_item_row.qty):
|
||||||
|
row.qty += returned_item_row.qty
|
||||||
|
returned_item_row.qty = 0
|
||||||
|
else:
|
||||||
|
row.qty = 0
|
||||||
|
returned_item_row.qty += row.qty
|
||||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||||
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||||
if flt(row.qty) or row.base_amount:
|
if flt(row.qty) or row.base_amount:
|
||||||
@@ -734,6 +741,8 @@ class GrossProfitGenerator(object):
|
|||||||
if self.filters.to_date:
|
if self.filters.to_date:
|
||||||
conditions += " and posting_date <= %(to_date)s"
|
conditions += " and posting_date <= %(to_date)s"
|
||||||
|
|
||||||
|
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
|
||||||
|
|
||||||
if self.filters.item_group:
|
if self.filters.item_group:
|
||||||
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))
|
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))
|
||||||
|
|
||||||
|
|||||||
@@ -381,3 +381,82 @@ class TestGrossProfit(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||||
|
|
||||||
|
def test_crnote_against_invoice_with_multiple_instances_of_same_item(self):
|
||||||
|
"""
|
||||||
|
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
|
||||||
|
"""
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||||
|
|
||||||
|
# Invoice with an item added twice
|
||||||
|
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
|
||||||
|
sinv.append("items", frappe.copy_doc(sinv.items[0], ignore_no_copy=False))
|
||||||
|
sinv = sinv.save().submit()
|
||||||
|
|
||||||
|
# Create Credit Note for Invoice
|
||||||
|
cr_note = make_sales_return(sinv.name)
|
||||||
|
cr_note = cr_note.save().submit()
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||||
|
)
|
||||||
|
|
||||||
|
columns, data = execute(filters=filters)
|
||||||
|
expected_entry = {
|
||||||
|
"parent_invoice": sinv.name,
|
||||||
|
"currency": "INR",
|
||||||
|
"sales_invoice": self.item,
|
||||||
|
"customer": self.customer,
|
||||||
|
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||||
|
"item_code": self.item,
|
||||||
|
"item_name": self.item,
|
||||||
|
"warehouse": "Stores - _GP",
|
||||||
|
"qty": 0.0,
|
||||||
|
"avg._selling_rate": 0.0,
|
||||||
|
"valuation_rate": 0.0,
|
||||||
|
"selling_amount": -100.0,
|
||||||
|
"buying_amount": 0.0,
|
||||||
|
"gross_profit": -100.0,
|
||||||
|
"gross_profit_%": 100.0,
|
||||||
|
}
|
||||||
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||||
|
# Both items of Invoice should have '0' qty
|
||||||
|
self.assertEqual(len(gp_entry), 2)
|
||||||
|
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||||
|
self.assertDictContainsSubset(expected_entry, gp_entry[1])
|
||||||
|
|
||||||
|
def test_standalone_cr_notes(self):
|
||||||
|
"""
|
||||||
|
Standalone cr notes will be reported as usual
|
||||||
|
"""
|
||||||
|
# Make Cr Note
|
||||||
|
sinv = self.create_sales_invoice(
|
||||||
|
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||||
|
)
|
||||||
|
sinv.is_return = 1
|
||||||
|
sinv = sinv.save().submit()
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||||
|
)
|
||||||
|
|
||||||
|
columns, data = execute(filters=filters)
|
||||||
|
expected_entry = {
|
||||||
|
"parent_invoice": sinv.name,
|
||||||
|
"currency": "INR",
|
||||||
|
"sales_invoice": self.item,
|
||||||
|
"customer": self.customer,
|
||||||
|
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||||
|
"item_code": self.item,
|
||||||
|
"item_name": self.item,
|
||||||
|
"warehouse": "Stores - _GP",
|
||||||
|
"qty": -1.0,
|
||||||
|
"avg._selling_rate": 100.0,
|
||||||
|
"valuation_rate": 0.0,
|
||||||
|
"selling_amount": -100.0,
|
||||||
|
"buying_amount": 0.0,
|
||||||
|
"gross_profit": -100.0,
|
||||||
|
"gross_profit_%": 100.0,
|
||||||
|
}
|
||||||
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||||
|
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||||
|
|||||||
@@ -375,12 +375,19 @@ class Asset(AccountsController):
|
|||||||
value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount"))
|
value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount"))
|
||||||
|
|
||||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||||
if finance_book.expected_value_after_useful_life and (
|
if (
|
||||||
(
|
finance_book.expected_value_after_useful_life
|
||||||
n == cint(number_of_pending_depreciations) - 1
|
and (
|
||||||
and value_after_depreciation != finance_book.expected_value_after_useful_life
|
(
|
||||||
|
n == cint(number_of_pending_depreciations) - 1
|
||||||
|
and value_after_depreciation != finance_book.expected_value_after_useful_life
|
||||||
|
)
|
||||||
|
or value_after_depreciation < finance_book.expected_value_after_useful_life
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
not self.flags.increase_in_asset_value_due_to_repair
|
||||||
|
or not finance_book.depreciation_method in ("Written Down Value", "Double Declining Balance")
|
||||||
)
|
)
|
||||||
or value_after_depreciation < finance_book.expected_value_after_useful_life
|
|
||||||
):
|
):
|
||||||
depreciation_amount += value_after_depreciation - finance_book.expected_value_after_useful_life
|
depreciation_amount += value_after_depreciation - finance_book.expected_value_after_useful_life
|
||||||
skip_row = True
|
skip_row = True
|
||||||
@@ -1175,17 +1182,21 @@ def get_total_days(date, frequency):
|
|||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def get_depreciation_amount(asset, depreciable_value, row):
|
def get_depreciation_amount(asset, depreciable_value, row):
|
||||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||||
# if the Depreciation Schedule is being prepared for the first time
|
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
|
||||||
if not asset.flags.increase_in_asset_life:
|
if asset.flags.increase_in_asset_life:
|
||||||
depreciation_amount = (
|
|
||||||
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
|
||||||
) / flt(row.total_number_of_depreciations)
|
|
||||||
|
|
||||||
# if the Depreciation Schedule is being modified after Asset Repair
|
|
||||||
else:
|
|
||||||
depreciation_amount = (
|
depreciation_amount = (
|
||||||
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||||
) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
|
) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
|
||||||
|
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
|
||||||
|
elif asset.flags.increase_in_asset_value_due_to_repair:
|
||||||
|
depreciation_amount = (
|
||||||
|
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
|
||||||
|
) / flt(row.total_number_of_depreciations)
|
||||||
|
# if the Depreciation Schedule is being prepared for the first time
|
||||||
|
else:
|
||||||
|
depreciation_amount = (
|
||||||
|
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||||
|
) / flt(row.total_number_of_depreciations)
|
||||||
else:
|
else:
|
||||||
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
|
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
|
||||||
|
|
||||||
|
|||||||
@@ -39,43 +39,51 @@ class AssetRepair(AccountsController):
|
|||||||
def before_submit(self):
|
def before_submit(self):
|
||||||
self.check_repair_status()
|
self.check_repair_status()
|
||||||
|
|
||||||
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
|
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
|
||||||
self.increase_asset_value()
|
|
||||||
if self.get("stock_consumption"):
|
|
||||||
self.check_for_stock_items_and_warehouse()
|
|
||||||
self.decrease_stock_quantity()
|
|
||||||
if self.get("capitalize_repair_cost"):
|
|
||||||
self.make_gl_entries()
|
|
||||||
if (
|
|
||||||
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
|
|
||||||
and self.increase_in_asset_life
|
|
||||||
):
|
|
||||||
self.modify_depreciation_schedule()
|
|
||||||
|
|
||||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
|
||||||
self.asset_doc.prepare_depreciation_data()
|
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
|
||||||
self.asset_doc.save()
|
|
||||||
|
self.increase_asset_value()
|
||||||
|
|
||||||
|
if self.get("stock_consumption"):
|
||||||
|
self.check_for_stock_items_and_warehouse()
|
||||||
|
self.decrease_stock_quantity()
|
||||||
|
if self.get("capitalize_repair_cost"):
|
||||||
|
self.make_gl_entries()
|
||||||
|
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
||||||
|
self.modify_depreciation_schedule()
|
||||||
|
|
||||||
|
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||||
|
self.asset_doc.prepare_depreciation_data()
|
||||||
|
if self.asset_doc.calculate_depreciation:
|
||||||
|
self.update_asset_expected_value_after_useful_life()
|
||||||
|
self.asset_doc.save()
|
||||||
|
|
||||||
def before_cancel(self):
|
def before_cancel(self):
|
||||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||||
|
|
||||||
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
|
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
|
||||||
self.decrease_asset_value()
|
|
||||||
if self.get("stock_consumption"):
|
|
||||||
self.increase_stock_quantity()
|
|
||||||
if self.get("capitalize_repair_cost"):
|
|
||||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
|
||||||
self.make_gl_entries(cancel=True)
|
|
||||||
self.db_set("stock_entry", None)
|
|
||||||
if (
|
|
||||||
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
|
|
||||||
and self.increase_in_asset_life
|
|
||||||
):
|
|
||||||
self.revert_depreciation_schedule_on_cancellation()
|
|
||||||
|
|
||||||
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
|
||||||
self.asset_doc.prepare_depreciation_data()
|
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
|
||||||
self.asset_doc.save()
|
|
||||||
|
self.decrease_asset_value()
|
||||||
|
|
||||||
|
if self.get("stock_consumption"):
|
||||||
|
self.increase_stock_quantity()
|
||||||
|
if self.get("capitalize_repair_cost"):
|
||||||
|
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||||
|
self.make_gl_entries(cancel=True)
|
||||||
|
self.db_set("stock_entry", None)
|
||||||
|
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
|
||||||
|
self.revert_depreciation_schedule_on_cancellation()
|
||||||
|
|
||||||
|
self.asset_doc.flags.ignore_validate_update_after_submit = True
|
||||||
|
self.asset_doc.prepare_depreciation_data()
|
||||||
|
if self.asset_doc.calculate_depreciation:
|
||||||
|
self.update_asset_expected_value_after_useful_life()
|
||||||
|
self.asset_doc.save()
|
||||||
|
|
||||||
def after_delete(self):
|
def after_delete(self):
|
||||||
frappe.get_doc("Asset", self.asset).set_status()
|
frappe.get_doc("Asset", self.asset).set_status()
|
||||||
@@ -95,6 +103,26 @@ class AssetRepair(AccountsController):
|
|||||||
title=_("Missing Warehouse"),
|
title=_("Missing Warehouse"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def update_asset_expected_value_after_useful_life(self):
|
||||||
|
for row in self.asset_doc.get("finance_books"):
|
||||||
|
if row.depreciation_method in ("Written Down Value", "Double Declining Balance"):
|
||||||
|
accumulated_depreciation_after_full_schedule = [
|
||||||
|
d.accumulated_depreciation_amount
|
||||||
|
for d in self.asset_doc.get("schedules")
|
||||||
|
if cint(d.finance_book_id) == row.idx
|
||||||
|
]
|
||||||
|
|
||||||
|
accumulated_depreciation_after_full_schedule = max(
|
||||||
|
accumulated_depreciation_after_full_schedule
|
||||||
|
)
|
||||||
|
|
||||||
|
asset_value_after_full_schedule = flt(
|
||||||
|
flt(row.value_after_depreciation) - flt(accumulated_depreciation_after_full_schedule),
|
||||||
|
row.precision("expected_value_after_useful_life"),
|
||||||
|
)
|
||||||
|
|
||||||
|
row.expected_value_after_useful_life = asset_value_after_full_schedule
|
||||||
|
|
||||||
def increase_asset_value(self):
|
def increase_asset_value(self):
|
||||||
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
|
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"transaction_settings_section",
|
"transaction_settings_section",
|
||||||
"po_required",
|
"po_required",
|
||||||
"pr_required",
|
"pr_required",
|
||||||
|
"over_order_allowance",
|
||||||
"column_break_12",
|
"column_break_12",
|
||||||
"maintain_same_rate",
|
"maintain_same_rate",
|
||||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||||
@@ -156,6 +157,13 @@
|
|||||||
"fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
|
"fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Set Landed Cost Based on Purchase Invoice Rate"
|
"label": "Set Landed Cost Based on Purchase Invoice Rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
|
||||||
|
"fieldname": "over_order_allowance",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Over Order Allowance (%)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
@@ -163,7 +171,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-02-28 15:41:32.686805",
|
"modified": "2023-03-02 17:02:14.404622",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
|||||||
from erpnext.accounts.party import get_party_account, get_party_account_currency
|
from erpnext.accounts.party import get_party_account, get_party_account_currency
|
||||||
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
|
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
|
||||||
from erpnext.controllers.buying_controller import BuyingController
|
from erpnext.controllers.buying_controller import BuyingController
|
||||||
|
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||||
|
validate_against_blanket_order,
|
||||||
|
)
|
||||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||||
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
|
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
|
||||||
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
|
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
|
||||||
@@ -69,6 +72,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
self.validate_with_previous_doc()
|
self.validate_with_previous_doc()
|
||||||
self.validate_for_subcontracting()
|
self.validate_for_subcontracting()
|
||||||
self.validate_minimum_order_qty()
|
self.validate_minimum_order_qty()
|
||||||
|
validate_against_blanket_order(self)
|
||||||
|
|
||||||
if self.is_old_subcontracting_flow:
|
if self.is_old_subcontracting_flow:
|
||||||
self.validate_bom_for_subcontracting_items()
|
self.validate_bom_for_subcontracting_items()
|
||||||
|
|||||||
@@ -24,11 +24,19 @@ class calculate_taxes_and_totals(object):
|
|||||||
def __init__(self, doc: Document):
|
def __init__(self, doc: Document):
|
||||||
self.doc = doc
|
self.doc = doc
|
||||||
frappe.flags.round_off_applicable_accounts = []
|
frappe.flags.round_off_applicable_accounts = []
|
||||||
|
|
||||||
|
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
||||||
|
|
||||||
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
|
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
|
||||||
self.calculate()
|
self.calculate()
|
||||||
|
|
||||||
|
def filter_rows(self):
|
||||||
|
"""Exclude rows, that do not fulfill the filter criteria, from totals computation."""
|
||||||
|
items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items")))
|
||||||
|
return items
|
||||||
|
|
||||||
def calculate(self):
|
def calculate(self):
|
||||||
if not len(self.doc.get("items")):
|
if not len(self._items):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.discount_amount_applied = False
|
self.discount_amount_applied = False
|
||||||
@@ -70,7 +78,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
if hasattr(self.doc, "tax_withholding_net_total"):
|
if hasattr(self.doc, "tax_withholding_net_total"):
|
||||||
sum_net_amount = 0
|
sum_net_amount = 0
|
||||||
sum_base_net_amount = 0
|
sum_base_net_amount = 0
|
||||||
for item in self.doc.get("items"):
|
for item in self._items:
|
||||||
if hasattr(item, "apply_tds") and item.apply_tds:
|
if hasattr(item, "apply_tds") and item.apply_tds:
|
||||||
sum_net_amount += item.net_amount
|
sum_net_amount += item.net_amount
|
||||||
sum_base_net_amount += item.base_net_amount
|
sum_base_net_amount += item.base_net_amount
|
||||||
@@ -79,7 +87,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
self.doc.base_tax_withholding_net_total = sum_base_net_amount
|
self.doc.base_tax_withholding_net_total = sum_base_net_amount
|
||||||
|
|
||||||
def validate_item_tax_template(self):
|
def validate_item_tax_template(self):
|
||||||
for item in self.doc.get("items"):
|
for item in self._items:
|
||||||
if item.item_code and item.get("item_tax_template"):
|
if item.item_code and item.get("item_tax_template"):
|
||||||
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
||||||
args = {
|
args = {
|
||||||
@@ -137,7 +145,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not self.discount_amount_applied:
|
if not self.discount_amount_applied:
|
||||||
for item in self.doc.get("items"):
|
for item in self._items:
|
||||||
self.doc.round_floats_in(item)
|
self.doc.round_floats_in(item)
|
||||||
|
|
||||||
if item.discount_percentage == 100:
|
if item.discount_percentage == 100:
|
||||||
@@ -236,7 +244,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
||||||
return
|
return
|
||||||
|
|
||||||
for item in self.doc.get("items"):
|
for item in self._items:
|
||||||
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
||||||
cumulated_tax_fraction = 0
|
cumulated_tax_fraction = 0
|
||||||
total_inclusive_tax_amount_per_qty = 0
|
total_inclusive_tax_amount_per_qty = 0
|
||||||
@@ -317,7 +325,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
self.doc.total
|
self.doc.total
|
||||||
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
|
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
|
||||||
|
|
||||||
for item in self.doc.get("items"):
|
for item in self._items:
|
||||||
self.doc.total += item.amount
|
self.doc.total += item.amount
|
||||||
self.doc.total_qty += item.qty
|
self.doc.total_qty += item.qty
|
||||||
self.doc.base_total += item.base_amount
|
self.doc.base_total += item.base_amount
|
||||||
@@ -354,7 +362,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
for n, item in enumerate(self.doc.get("items")):
|
for n, item in enumerate(self._items):
|
||||||
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
||||||
for i, tax in enumerate(self.doc.get("taxes")):
|
for i, tax in enumerate(self.doc.get("taxes")):
|
||||||
# tax_amount represents the amount of tax for the current step
|
# tax_amount represents the amount of tax for the current step
|
||||||
@@ -363,7 +371,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
# Adjust divisional loss to the last item
|
# Adjust divisional loss to the last item
|
||||||
if tax.charge_type == "Actual":
|
if tax.charge_type == "Actual":
|
||||||
actual_tax_dict[tax.idx] -= current_tax_amount
|
actual_tax_dict[tax.idx] -= current_tax_amount
|
||||||
if n == len(self.doc.get("items")) - 1:
|
if n == len(self._items) - 1:
|
||||||
current_tax_amount += actual_tax_dict[tax.idx]
|
current_tax_amount += actual_tax_dict[tax.idx]
|
||||||
|
|
||||||
# accumulate tax amount into tax.tax_amount
|
# accumulate tax amount into tax.tax_amount
|
||||||
@@ -391,7 +399,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# set precision in the last item iteration
|
# set precision in the last item iteration
|
||||||
if n == len(self.doc.get("items")) - 1:
|
if n == len(self._items) - 1:
|
||||||
self.round_off_totals(tax)
|
self.round_off_totals(tax)
|
||||||
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
|
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
|
||||||
|
|
||||||
@@ -570,7 +578,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
def calculate_total_net_weight(self):
|
def calculate_total_net_weight(self):
|
||||||
if self.doc.meta.get_field("total_net_weight"):
|
if self.doc.meta.get_field("total_net_weight"):
|
||||||
self.doc.total_net_weight = 0.0
|
self.doc.total_net_weight = 0.0
|
||||||
for d in self.doc.items:
|
for d in self._items:
|
||||||
if d.total_weight:
|
if d.total_weight:
|
||||||
self.doc.total_net_weight += d.total_weight
|
self.doc.total_net_weight += d.total_weight
|
||||||
|
|
||||||
@@ -630,7 +638,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
|
|
||||||
if total_for_discount_amount:
|
if total_for_discount_amount:
|
||||||
# calculate item amount after Discount Amount
|
# calculate item amount after Discount Amount
|
||||||
for i, item in enumerate(self.doc.get("items")):
|
for i, item in enumerate(self._items):
|
||||||
distributed_amount = (
|
distributed_amount = (
|
||||||
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
|
flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount
|
||||||
)
|
)
|
||||||
@@ -643,7 +651,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
self.doc.apply_discount_on == "Net Total"
|
self.doc.apply_discount_on == "Net Total"
|
||||||
or not taxes
|
or not taxes
|
||||||
or total_for_discount_amount == self.doc.net_total
|
or total_for_discount_amount == self.doc.net_total
|
||||||
) and i == len(self.doc.get("items")) - 1:
|
) and i == len(self._items) - 1:
|
||||||
discount_amount_loss = flt(
|
discount_amount_loss = flt(
|
||||||
self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total")
|
self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
get_shopping_cart_settings,
|
get_shopping_cart_settings,
|
||||||
@@ -166,6 +166,27 @@ def get_next_attribute_and_values(item_code, selected_attributes):
|
|||||||
else:
|
else:
|
||||||
product_info = None
|
product_info = None
|
||||||
|
|
||||||
|
product_id = ""
|
||||||
|
website_warehouse = ""
|
||||||
|
if exact_match or filtered_items:
|
||||||
|
if exact_match and len(exact_match) == 1:
|
||||||
|
product_id = exact_match[0]
|
||||||
|
elif filtered_items_count == 1:
|
||||||
|
product_id = list(filtered_items)[0]
|
||||||
|
|
||||||
|
if product_id:
|
||||||
|
website_warehouse = frappe.get_cached_value(
|
||||||
|
"Website Item", {"item_code": product_id}, "website_warehouse"
|
||||||
|
)
|
||||||
|
|
||||||
|
available_qty = 0.0
|
||||||
|
if website_warehouse:
|
||||||
|
available_qty = flt(
|
||||||
|
frappe.db.get_value(
|
||||||
|
"Bin", {"item_code": product_id, "warehouse": website_warehouse}, "actual_qty"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"next_attribute": next_attribute,
|
"next_attribute": next_attribute,
|
||||||
"valid_options_for_attributes": valid_options_for_attributes,
|
"valid_options_for_attributes": valid_options_for_attributes,
|
||||||
@@ -173,6 +194,7 @@ def get_next_attribute_and_values(item_code, selected_attributes):
|
|||||||
"filtered_items": filtered_items if filtered_items_count < 10 else [],
|
"filtered_items": filtered_items if filtered_items_count < 10 else [],
|
||||||
"exact_match": exact_match,
|
"exact_match": exact_match,
|
||||||
"product_info": product_info,
|
"product_info": product_info,
|
||||||
|
"available_qty": available_qty,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class PlaidConnector:
|
|||||||
def __init__(self, access_token=None):
|
def __init__(self, access_token=None):
|
||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
self.settings = frappe.get_single("Plaid Settings")
|
self.settings = frappe.get_single("Plaid Settings")
|
||||||
self.products = ["auth", "transactions"]
|
self.products = ["transactions"]
|
||||||
self.client_name = frappe.local.site
|
self.client_name = frappe.local.site
|
||||||
self.client = plaid.Client(
|
self.client = plaid.Client(
|
||||||
client_id=self.settings.plaid_client_id,
|
client_id=self.settings.plaid_client_id,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ erpnext.integrations.plaidLink = class plaidLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init_config() {
|
async init_config() {
|
||||||
this.product = ["auth", "transactions"];
|
this.product = ["transactions"];
|
||||||
this.plaid_env = this.frm.doc.plaid_env;
|
this.plaid_env = this.frm.doc.plaid_env;
|
||||||
this.client_name = frappe.boot.sitename;
|
this.client_name = frappe.boot.sitename;
|
||||||
this.token = await this.get_link_token();
|
this.token = await this.get_link_token();
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ def add_bank_accounts(response, bank, company):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
bank = json.loads(bank)
|
if isinstance(bank, str):
|
||||||
|
bank = json.loads(bank)
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
default_gl_account = get_default_bank_cash_account(company, "Bank")
|
default_gl_account = get_default_bank_cash_account(company, "Bank")
|
||||||
@@ -177,16 +178,15 @@ def sync_transactions(bank, bank_account):
|
|||||||
)
|
)
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for transaction in reversed(transactions):
|
if transactions:
|
||||||
result += new_bank_transaction(transaction)
|
for transaction in reversed(transactions):
|
||||||
|
result += new_bank_transaction(transaction)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
last_transaction_date = frappe.db.get_value("Bank Transaction", result.pop(), "date")
|
last_transaction_date = frappe.db.get_value("Bank Transaction", result.pop(), "date")
|
||||||
|
|
||||||
frappe.logger().info(
|
frappe.logger().info(
|
||||||
"Plaid added {} new Bank Transactions from '{}' between {} and {}".format(
|
f"Plaid added {len(result)} new Bank Transactions from '{bank_account}' between {start_date} and {end_date}"
|
||||||
len(result), bank_account, start_date, end_date
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
@@ -230,19 +230,20 @@ def new_bank_transaction(transaction):
|
|||||||
|
|
||||||
bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"]))
|
bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"]))
|
||||||
|
|
||||||
if float(transaction["amount"]) >= 0:
|
amount = float(transaction["amount"])
|
||||||
debit = 0
|
if amount >= 0.0:
|
||||||
credit = float(transaction["amount"])
|
deposit = 0.0
|
||||||
|
withdrawal = amount
|
||||||
else:
|
else:
|
||||||
debit = abs(float(transaction["amount"]))
|
deposit = abs(amount)
|
||||||
credit = 0
|
withdrawal = 0.0
|
||||||
|
|
||||||
status = "Pending" if transaction["pending"] == "True" else "Settled"
|
status = "Pending" if transaction["pending"] == "True" else "Settled"
|
||||||
|
|
||||||
tags = []
|
tags = []
|
||||||
try:
|
try:
|
||||||
tags += transaction["category"]
|
tags += transaction["category"]
|
||||||
tags += ["Plaid Cat. {}".format(transaction["category_id"])]
|
tags += [f'Plaid Cat. {transaction["category_id"]}']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -254,11 +255,18 @@ def new_bank_transaction(transaction):
|
|||||||
"date": getdate(transaction["date"]),
|
"date": getdate(transaction["date"]),
|
||||||
"status": status,
|
"status": status,
|
||||||
"bank_account": bank_account,
|
"bank_account": bank_account,
|
||||||
"deposit": debit,
|
"deposit": deposit,
|
||||||
"withdrawal": credit,
|
"withdrawal": withdrawal,
|
||||||
"currency": transaction["iso_currency_code"],
|
"currency": transaction["iso_currency_code"],
|
||||||
"transaction_id": transaction["transaction_id"],
|
"transaction_id": transaction["transaction_id"],
|
||||||
"reference_number": transaction["payment_meta"]["reference_number"],
|
"transaction_type": (
|
||||||
|
transaction["transaction_code"] or transaction["payment_meta"]["payment_method"]
|
||||||
|
),
|
||||||
|
"reference_number": (
|
||||||
|
transaction["check_number"]
|
||||||
|
or transaction["payment_meta"]["reference_number"]
|
||||||
|
or transaction["name"]
|
||||||
|
),
|
||||||
"description": transaction["name"],
|
"description": transaction["name"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -271,7 +279,7 @@ def new_bank_transaction(transaction):
|
|||||||
result.append(new_transaction.name)
|
result.append(new_transaction.name)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
frappe.throw(title=_("Bank transaction creation error"))
|
frappe.throw(_("Bank transaction creation error"))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -300,3 +308,26 @@ def enqueue_synchronization():
|
|||||||
def get_link_token_for_update(access_token):
|
def get_link_token_for_update(access_token):
|
||||||
plaid = PlaidConnector(access_token)
|
plaid = PlaidConnector(access_token)
|
||||||
return plaid.get_link_token(update_mode=True)
|
return plaid.get_link_token(update_mode=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_company(bank_account_name):
|
||||||
|
from frappe.defaults import get_user_default
|
||||||
|
|
||||||
|
company_names = frappe.db.get_all("Company", pluck="name")
|
||||||
|
if len(company_names) == 1:
|
||||||
|
return company_names[0]
|
||||||
|
if frappe.db.exists("Bank Account", bank_account_name):
|
||||||
|
return frappe.db.get_value("Bank Account", bank_account_name, "company")
|
||||||
|
company_default = get_user_default("Company")
|
||||||
|
if company_default:
|
||||||
|
return company_default
|
||||||
|
frappe.throw(_("Could not detect the Company for updating Bank Accounts"))
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def update_bank_account_ids(response):
|
||||||
|
data = json.loads(response)
|
||||||
|
institution_name = data["institution"]["name"]
|
||||||
|
bank = frappe.get_doc("Bank", institution_name).as_dict()
|
||||||
|
bank_account_name = f"{data['account']['name']} - {institution_name}"
|
||||||
|
return add_bank_accounts(response, bank, get_company(bank_account_name))
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ class TestPlaidSettings(unittest.TestCase):
|
|||||||
"unofficial_currency_code": None,
|
"unofficial_currency_code": None,
|
||||||
"name": "INTRST PYMNT",
|
"name": "INTRST PYMNT",
|
||||||
"transaction_type": "place",
|
"transaction_type": "place",
|
||||||
|
"transaction_code": "direct debit",
|
||||||
|
"check_number": "3456789",
|
||||||
"amount": -4.22,
|
"amount": -4.22,
|
||||||
"location": {
|
"location": {
|
||||||
"city": None,
|
"city": None,
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ frappe.ui.form.on('Blanket Order', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
|
frm.custom_make_buttons = {
|
||||||
|
'Purchase Order': 'Purchase Order',
|
||||||
|
'Sales Order': 'Sales Order',
|
||||||
|
'Quotation': 'Quotation',
|
||||||
|
};
|
||||||
|
|
||||||
frm.add_fetch("customer", "customer_name", "customer_name");
|
frm.add_fetch("customer", "customer_name", "customer_name");
|
||||||
frm.add_fetch("supplier", "supplier_name", "supplier_name");
|
frm.add_fetch("supplier", "supplier_name", "supplier_name");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import flt, getdate
|
from frappe.utils import flt, getdate
|
||||||
|
|
||||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||||
@@ -29,21 +30,23 @@ class BlanketOrder(Document):
|
|||||||
|
|
||||||
def update_ordered_qty(self):
|
def update_ordered_qty(self):
|
||||||
ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order"
|
ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order"
|
||||||
|
|
||||||
|
trans = frappe.qb.DocType(ref_doctype)
|
||||||
|
trans_item = frappe.qb.DocType(f"{ref_doctype} Item")
|
||||||
|
|
||||||
item_ordered_qty = frappe._dict(
|
item_ordered_qty = frappe._dict(
|
||||||
frappe.db.sql(
|
(
|
||||||
"""
|
frappe.qb.from_(trans_item)
|
||||||
select trans_item.item_code, sum(trans_item.stock_qty) as qty
|
.from_(trans)
|
||||||
from `tab{0} Item` trans_item, `tab{0}` trans
|
.select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty"))
|
||||||
where trans.name = trans_item.parent
|
.where(
|
||||||
and trans_item.blanket_order=%s
|
(trans.name == trans_item.parent)
|
||||||
and trans.docstatus=1
|
& (trans_item.blanket_order == self.name)
|
||||||
and trans.status not in ('Closed', 'Stopped')
|
& (trans.docstatus == 1)
|
||||||
group by trans_item.item_code
|
& (trans.status.notin(["Stopped", "Closed"]))
|
||||||
""".format(
|
)
|
||||||
ref_doctype
|
.groupby(trans_item.item_code)
|
||||||
),
|
).run()
|
||||||
self.name,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for d in self.items:
|
for d in self.items:
|
||||||
@@ -79,7 +82,43 @@ def make_order(source_name):
|
|||||||
"doctype": doctype + " Item",
|
"doctype": doctype + " Item",
|
||||||
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
|
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
|
"condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return target_doc
|
return target_doc
|
||||||
|
|
||||||
|
|
||||||
|
def validate_against_blanket_order(order_doc):
|
||||||
|
if order_doc.doctype in ("Sales Order", "Purchase Order"):
|
||||||
|
order_data = {}
|
||||||
|
|
||||||
|
for item in order_doc.get("items"):
|
||||||
|
if item.against_blanket_order and item.blanket_order:
|
||||||
|
if item.blanket_order in order_data:
|
||||||
|
if item.item_code in order_data[item.blanket_order]:
|
||||||
|
order_data[item.blanket_order][item.item_code] += item.qty
|
||||||
|
else:
|
||||||
|
order_data[item.blanket_order][item.item_code] = item.qty
|
||||||
|
else:
|
||||||
|
order_data[item.blanket_order] = {item.item_code: item.qty}
|
||||||
|
|
||||||
|
if order_data:
|
||||||
|
allowance = flt(
|
||||||
|
frappe.db.get_single_value(
|
||||||
|
"Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings",
|
||||||
|
"over_order_allowance",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for bo_name, item_data in order_data.items():
|
||||||
|
bo_doc = frappe.get_doc("Blanket Order", bo_name)
|
||||||
|
for item in bo_doc.get("items"):
|
||||||
|
if item.item_code in item_data:
|
||||||
|
remaining_qty = item.qty - item.ordered_qty
|
||||||
|
allowed_qty = remaining_qty + (remaining_qty * (allowance / 100))
|
||||||
|
if allowed_qty < item_data[item.item_code]:
|
||||||
|
frappe.throw(
|
||||||
|
_("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format(
|
||||||
|
item.item_code, allowed_qty, bo_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -63,6 +63,33 @@ class TestBlanketOrder(FrappeTestCase):
|
|||||||
po1.currency = get_company_currency(po1.company)
|
po1.currency = get_company_currency(po1.company)
|
||||||
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
|
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
|
||||||
|
|
||||||
|
def test_over_order_allowance(self):
|
||||||
|
# Sales Order
|
||||||
|
bo = make_blanket_order(blanket_order_type="Selling", quantity=100)
|
||||||
|
|
||||||
|
frappe.flags.args.doctype = "Sales Order"
|
||||||
|
so = make_order(bo.name)
|
||||||
|
so.currency = get_company_currency(so.company)
|
||||||
|
so.delivery_date = today()
|
||||||
|
so.items[0].qty = 110
|
||||||
|
self.assertRaises(frappe.ValidationError, so.submit)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10)
|
||||||
|
so.submit()
|
||||||
|
|
||||||
|
# Purchase Order
|
||||||
|
bo = make_blanket_order(blanket_order_type="Purchasing", quantity=100)
|
||||||
|
|
||||||
|
frappe.flags.args.doctype = "Purchase Order"
|
||||||
|
po = make_order(bo.name)
|
||||||
|
po.currency = get_company_currency(po.company)
|
||||||
|
po.schedule_date = today()
|
||||||
|
po.items[0].qty = 110
|
||||||
|
self.assertRaises(frappe.ValidationError, po.submit)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10)
|
||||||
|
po.submit()
|
||||||
|
|
||||||
|
|
||||||
def make_blanket_order(**args):
|
def make_blanket_order(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from collections import deque
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, timeout
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
|
|
||||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||||
@@ -27,6 +27,7 @@ test_dependencies = ["Item", "Quality Inspection Template"]
|
|||||||
|
|
||||||
|
|
||||||
class TestBOM(FrappeTestCase):
|
class TestBOM(FrappeTestCase):
|
||||||
|
@timeout
|
||||||
def test_get_items(self):
|
def test_get_items(self):
|
||||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
self.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict)
|
self.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict)
|
||||||
self.assertEqual(len(items_dict.values()), 2)
|
self.assertEqual(len(items_dict.values()), 2)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_get_items_exploded(self):
|
def test_get_items_exploded(self):
|
||||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||||
|
|
||||||
@@ -49,11 +51,13 @@ class TestBOM(FrappeTestCase):
|
|||||||
self.assertTrue(test_records[0]["items"][1]["item_code"] in items_dict)
|
self.assertTrue(test_records[0]["items"][1]["item_code"] in items_dict)
|
||||||
self.assertEqual(len(items_dict.values()), 3)
|
self.assertEqual(len(items_dict.values()), 3)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_get_items_list(self):
|
def test_get_items_list(self):
|
||||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items
|
from erpnext.manufacturing.doctype.bom.bom import get_bom_items
|
||||||
|
|
||||||
self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3)
|
self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_default_bom(self):
|
def test_default_bom(self):
|
||||||
def _get_default_bom_in_item():
|
def _get_default_bom_in_item():
|
||||||
return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"))
|
return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"))
|
||||||
@@ -71,6 +75,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertTrue(_get_default_bom_in_item(), bom.name)
|
self.assertTrue(_get_default_bom_in_item(), bom.name)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_update_bom_cost_in_all_boms(self):
|
def test_update_bom_cost_in_all_boms(self):
|
||||||
# get current rate for '_Test Item 2'
|
# get current rate for '_Test Item 2'
|
||||||
bom_rates = frappe.db.get_values(
|
bom_rates = frappe.db.get_values(
|
||||||
@@ -99,6 +104,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(d.base_rate, rm_base_rate + 10)
|
self.assertEqual(d.base_rate, rm_base_rate + 10)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_cost(self):
|
def test_bom_cost(self):
|
||||||
bom = frappe.copy_doc(test_records[2])
|
bom = frappe.copy_doc(test_records[2])
|
||||||
bom.insert()
|
bom.insert()
|
||||||
@@ -127,6 +133,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
||||||
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
|
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_cost_with_batch_size(self):
|
def test_bom_cost_with_batch_size(self):
|
||||||
bom = frappe.copy_doc(test_records[2])
|
bom = frappe.copy_doc(test_records[2])
|
||||||
bom.docstatus = 0
|
bom.docstatus = 0
|
||||||
@@ -145,6 +152,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
self.assertAlmostEqual(bom.operating_cost, op_cost / 2)
|
self.assertAlmostEqual(bom.operating_cost, op_cost / 2)
|
||||||
bom.delete()
|
bom.delete()
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
|
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
|
||||||
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
|
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
|
||||||
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):
|
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):
|
||||||
@@ -181,6 +189,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
self.assertEqual(bom.base_raw_material_cost, 27000)
|
self.assertEqual(bom.base_raw_material_cost, 27000)
|
||||||
self.assertEqual(bom.base_total_cost, 33000)
|
self.assertEqual(bom.base_total_cost, 33000)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_cost_multi_uom_based_on_valuation_rate(self):
|
def test_bom_cost_multi_uom_based_on_valuation_rate(self):
|
||||||
bom = frappe.copy_doc(test_records[2])
|
bom = frappe.copy_doc(test_records[2])
|
||||||
bom.set_rate_of_sub_assembly_item_based_on_bom = 0
|
bom.set_rate_of_sub_assembly_item_based_on_bom = 0
|
||||||
@@ -202,6 +211,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(bom.items[0].rate, 20)
|
self.assertEqual(bom.items[0].rate, 20)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_cost_with_fg_based_operating_cost(self):
|
def test_bom_cost_with_fg_based_operating_cost(self):
|
||||||
bom = frappe.copy_doc(test_records[4])
|
bom = frappe.copy_doc(test_records[4])
|
||||||
bom.insert()
|
bom.insert()
|
||||||
@@ -229,6 +239,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
||||||
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
|
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_subcontractor_sourced_item(self):
|
def test_subcontractor_sourced_item(self):
|
||||||
item_code = "_Test Subcontracted FG Item 1"
|
item_code = "_Test Subcontracted FG Item 1"
|
||||||
set_backflush_based_on("Material Transferred for Subcontract")
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
@@ -310,6 +321,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
supplied_items = sorted([d.rm_item_code for d in sco.supplied_items])
|
supplied_items = sorted([d.rm_item_code for d in sco.supplied_items])
|
||||||
self.assertEqual(bom_items, supplied_items)
|
self.assertEqual(bom_items, supplied_items)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_tree_representation(self):
|
def test_bom_tree_representation(self):
|
||||||
bom_tree = {
|
bom_tree = {
|
||||||
"Assembly": {
|
"Assembly": {
|
||||||
@@ -335,6 +347,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
for reqd_item, created_item in zip(reqd_order, created_order):
|
for reqd_item, created_item in zip(reqd_order, created_order):
|
||||||
self.assertEqual(reqd_item, created_item.item_code)
|
self.assertEqual(reqd_item, created_item.item_code)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_generated_variant_bom(self):
|
def test_generated_variant_bom(self):
|
||||||
from erpnext.controllers.item_variant import create_variant
|
from erpnext.controllers.item_variant import create_variant
|
||||||
|
|
||||||
@@ -375,6 +388,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
self.assertEqual(reqd_item.qty, created_item.qty)
|
self.assertEqual(reqd_item.qty, created_item.qty)
|
||||||
self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
|
self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_recursion_1st_level(self):
|
def test_bom_recursion_1st_level(self):
|
||||||
"""BOM should not allow BOM item again in child"""
|
"""BOM should not allow BOM item again in child"""
|
||||||
item_code = make_item(properties={"is_stock_item": 1}).name
|
item_code = make_item(properties={"is_stock_item": 1}).name
|
||||||
@@ -387,6 +401,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
bom.items[0].bom_no = bom.name
|
bom.items[0].bom_no = bom.name
|
||||||
bom.save()
|
bom.save()
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_recursion_transitive(self):
|
def test_bom_recursion_transitive(self):
|
||||||
item1 = make_item(properties={"is_stock_item": 1}).name
|
item1 = make_item(properties={"is_stock_item": 1}).name
|
||||||
item2 = make_item(properties={"is_stock_item": 1}).name
|
item2 = make_item(properties={"is_stock_item": 1}).name
|
||||||
@@ -408,6 +423,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
bom1.save()
|
bom1.save()
|
||||||
bom2.save()
|
bom2.save()
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_with_process_loss_item(self):
|
def test_bom_with_process_loss_item(self):
|
||||||
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
||||||
|
|
||||||
@@ -421,6 +437,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
# Items with whole UOMs can't be PL Items
|
# Items with whole UOMs can't be PL Items
|
||||||
self.assertRaises(frappe.ValidationError, bom_doc.submit)
|
self.assertRaises(frappe.ValidationError, bom_doc.submit)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_item_query(self):
|
def test_bom_item_query(self):
|
||||||
query = partial(
|
query = partial(
|
||||||
item_query,
|
item_query,
|
||||||
@@ -440,6 +457,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
|
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_exclude_exploded_items_from_bom(self):
|
def test_exclude_exploded_items_from_bom(self):
|
||||||
bom_no = get_default_bom()
|
bom_no = get_default_bom()
|
||||||
new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no))
|
new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no))
|
||||||
@@ -458,6 +476,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
|
|
||||||
new_bom.delete()
|
new_bom.delete()
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_valid_transfer_defaults(self):
|
def test_valid_transfer_defaults(self):
|
||||||
bom_with_op = frappe.db.get_value(
|
bom_with_op = frappe.db.get_value(
|
||||||
"BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1}
|
"BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1}
|
||||||
@@ -489,11 +508,13 @@ class TestBOM(FrappeTestCase):
|
|||||||
self.assertEqual(bom.transfer_material_against, "Work Order")
|
self.assertEqual(bom.transfer_material_against, "Work Order")
|
||||||
bom.delete()
|
bom.delete()
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_name_length(self):
|
def test_bom_name_length(self):
|
||||||
"""test >140 char names"""
|
"""test >140 char names"""
|
||||||
bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}}
|
bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}}
|
||||||
create_nested_bom(bom_tree, prefix="")
|
create_nested_bom(bom_tree, prefix="")
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_version_index(self):
|
def test_version_index(self):
|
||||||
|
|
||||||
bom = frappe.new_doc("BOM")
|
bom = frappe.new_doc("BOM")
|
||||||
@@ -515,6 +536,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
msg=f"Incorrect index for {existing_boms}",
|
msg=f"Incorrect index for {existing_boms}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_versioning(self):
|
def test_bom_versioning(self):
|
||||||
bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}}
|
bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}}
|
||||||
bom = create_nested_bom(bom_tree, prefix="")
|
bom = create_nested_bom(bom_tree, prefix="")
|
||||||
@@ -547,6 +569,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
self.assertNotEqual(amendment.name, version.name)
|
self.assertNotEqual(amendment.name, version.name)
|
||||||
self.assertEqual(int(version.name.split("-")[-1]), 2)
|
self.assertEqual(int(version.name.split("-")[-1]), 2)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_clear_inpection_quality(self):
|
def test_clear_inpection_quality(self):
|
||||||
|
|
||||||
bom = frappe.copy_doc(test_records[2], ignore_no_copy=True)
|
bom = frappe.copy_doc(test_records[2], ignore_no_copy=True)
|
||||||
@@ -565,6 +588,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(bom.quality_inspection_template, None)
|
self.assertEqual(bom.quality_inspection_template, None)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_pricing_based_on_lpp(self):
|
def test_bom_pricing_based_on_lpp(self):
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
@@ -585,6 +609,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
bom.submit()
|
bom.submit()
|
||||||
self.assertEqual(bom.items[0].rate, 42)
|
self.assertEqual(bom.items[0].rate, 42)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_set_default_bom_for_item_having_single_bom(self):
|
def test_set_default_bom_for_item_having_single_bom(self):
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
@@ -621,6 +646,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
bom.reload()
|
bom.reload()
|
||||||
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
|
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_exploded_items_rate(self):
|
def test_exploded_items_rate(self):
|
||||||
rm_item = make_item(
|
rm_item = make_item(
|
||||||
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
|
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
|
||||||
@@ -649,6 +675,7 @@ class TestBOM(FrappeTestCase):
|
|||||||
bom.submit()
|
bom.submit()
|
||||||
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
|
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_cost_update_flag(self):
|
def test_bom_cost_update_flag(self):
|
||||||
rm_item = make_item(
|
rm_item = make_item(
|
||||||
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
|
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, timeout
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
|
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
|
||||||
update_cost_in_all_boms_in_test,
|
update_cost_in_all_boms_in_test,
|
||||||
@@ -20,6 +20,7 @@ class TestBOMUpdateTool(FrappeTestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_replace_bom(self):
|
def test_replace_bom(self):
|
||||||
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ class TestBOMUpdateTool(FrappeTestCase):
|
|||||||
self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1}))
|
self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1}))
|
||||||
self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1}))
|
self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1}))
|
||||||
|
|
||||||
|
@timeout
|
||||||
def test_bom_cost(self):
|
def test_bom_cost(self):
|
||||||
for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
|
for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
|
||||||
item_doc = create_item(item, valuation_rate=100)
|
item_doc = create_item(item, valuation_rate=100)
|
||||||
|
|||||||
@@ -325,5 +325,6 @@ erpnext.patches.v14_0.update_entry_type_for_journal_entry
|
|||||||
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
|
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
|
||||||
erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries
|
erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries
|
||||||
erpnext.patches.v14_0.set_pick_list_status
|
erpnext.patches.v14_0.set_pick_list_status
|
||||||
|
erpnext.patches.v13_0.update_docs_link
|
||||||
# below migration patches should always run last
|
# below migration patches should always run last
|
||||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||||
|
|||||||
14
erpnext/patches/v13_0/update_docs_link.py
Normal file
14
erpnext/patches/v13_0/update_docs_link.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: MIT. See LICENSE
|
||||||
|
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
navbar_settings = frappe.get_single("Navbar Settings")
|
||||||
|
for item in navbar_settings.help_dropdown:
|
||||||
|
if item.is_standard and item.route == "https://erpnext.com/docs/user/manual":
|
||||||
|
item.route = "https://docs.erpnext.com/docs/v14/user/manual/en/introduction"
|
||||||
|
|
||||||
|
navbar_settings.save()
|
||||||
@@ -7,6 +7,9 @@ from erpnext.setup.utils import get_exchange_rate
|
|||||||
|
|
||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
|
frappe.reload_doc(
|
||||||
|
"accounts", "doctype", "currency_exchange_settings"
|
||||||
|
) # get_exchange_rate depends on Currency Exchange Settings
|
||||||
frappe.reload_doctype("Opportunity")
|
frappe.reload_doctype("Opportunity")
|
||||||
opportunities = frappe.db.get_list(
|
opportunities = frappe.db.get_list(
|
||||||
"Opportunity",
|
"Opportunity",
|
||||||
|
|||||||
@@ -182,6 +182,9 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.transactions.splice(transaction_index, 1);
|
this.transactions.splice(transaction_index, 1);
|
||||||
|
for (const [k, v] of Object.entries(this.transaction_dt_map)) {
|
||||||
|
if (v > transaction_index) this.transaction_dt_map[k] = v - 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.datatable.refresh(this.transactions, this.columns);
|
this.datatable.refresh(this.transactions, this.columns);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
doctype: "Bank Transaction",
|
doctype: "Bank Transaction",
|
||||||
filters: { name: this.bank_transaction_name },
|
filters: { name: this.bank_transaction_name },
|
||||||
fieldname: [
|
fieldname: [
|
||||||
"date as reference_date",
|
"date",
|
||||||
"deposit",
|
"deposit",
|
||||||
"withdrawal",
|
"withdrawal",
|
||||||
"currency",
|
"currency",
|
||||||
@@ -33,6 +33,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
"party",
|
"party",
|
||||||
"unallocated_amount",
|
"unallocated_amount",
|
||||||
"allocated_amount",
|
"allocated_amount",
|
||||||
|
"transaction_type",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
@@ -41,11 +42,23 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
r.message.payment_entry = 1;
|
r.message.payment_entry = 1;
|
||||||
r.message.journal_entry = 1;
|
r.message.journal_entry = 1;
|
||||||
this.dialog.set_values(r.message);
|
this.dialog.set_values(r.message);
|
||||||
|
this.copy_data_to_voucher();
|
||||||
this.dialog.show();
|
this.dialog.show();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copy_data_to_voucher() {
|
||||||
|
let copied = {
|
||||||
|
reference_number: this.bank_transaction.reference_number || this.bank_transaction.description,
|
||||||
|
posting_date: this.bank_transaction.date,
|
||||||
|
reference_date: this.bank_transaction.date,
|
||||||
|
mode_of_payment: this.bank_transaction.transaction_type,
|
||||||
|
};
|
||||||
|
this.dialog.set_values(copied);
|
||||||
|
}
|
||||||
|
|
||||||
get_linked_vouchers(document_types) {
|
get_linked_vouchers(document_types) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method:
|
method:
|
||||||
@@ -75,10 +88,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
row[1],
|
row[1],
|
||||||
row[2],
|
row[2],
|
||||||
reference_date,
|
reference_date,
|
||||||
row[8],
|
|
||||||
format_currency(row[3], row[9]),
|
format_currency(row[3], row[9]),
|
||||||
row[6],
|
|
||||||
row[4],
|
row[4],
|
||||||
|
row[6],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
this.get_dt_columns();
|
this.get_dt_columns();
|
||||||
@@ -104,7 +116,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
{
|
{
|
||||||
name: __("Document Name"),
|
name: __("Document Name"),
|
||||||
editable: false,
|
editable: false,
|
||||||
width: 150,
|
width: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: __("Reference Date"),
|
name: __("Reference Date"),
|
||||||
@@ -112,25 +124,19 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Posting Date",
|
name: __("Remaining"),
|
||||||
editable: false,
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: __("Amount"),
|
|
||||||
editable: false,
|
editable: false,
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: __("Party"),
|
|
||||||
editable: false,
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
name: __("Reference Number"),
|
name: __("Reference Number"),
|
||||||
editable: false,
|
editable: false,
|
||||||
width: 140,
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: __("Party"),
|
||||||
|
editable: false,
|
||||||
|
width: 100,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -224,6 +230,16 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
fieldname: "exact_match",
|
fieldname: "exact_match",
|
||||||
onchange: () => this.update_options(),
|
onchange: () => this.update_options(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "column_break_5",
|
||||||
|
fieldtype: "Column Break",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Check",
|
||||||
|
label: "Bank Transaction",
|
||||||
|
fieldname: "bank_transaction",
|
||||||
|
onchange: () => this.update_options(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Section Break",
|
fieldtype: "Section Break",
|
||||||
fieldname: "section_break_1",
|
fieldname: "section_break_1",
|
||||||
@@ -289,7 +305,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
fieldtype: "Column Break",
|
fieldtype: "Column Break",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: "Journal Entry Type",
|
default: "Bank Entry",
|
||||||
fieldname: "journal_entry_type",
|
fieldname: "journal_entry_type",
|
||||||
fieldtype: "Select",
|
fieldtype: "Select",
|
||||||
label: "Journal Entry Type",
|
label: "Journal Entry Type",
|
||||||
@@ -364,20 +380,30 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
fieldtype: "Section Break",
|
fieldtype: "Section Break",
|
||||||
fieldname: "details_section",
|
fieldname: "details_section",
|
||||||
label: "Transaction Details",
|
label: "Transaction Details",
|
||||||
collapsible: 1,
|
},
|
||||||
|
{
|
||||||
|
fieldname: "date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
label: "Date",
|
||||||
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "deposit",
|
fieldname: "deposit",
|
||||||
fieldtype: "Currency",
|
fieldtype: "Currency",
|
||||||
label: "Deposit",
|
label: "Deposit",
|
||||||
options: "currency",
|
options: "account_currency",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "withdrawal",
|
fieldname: "withdrawal",
|
||||||
fieldtype: "Currency",
|
fieldtype: "Currency",
|
||||||
label: "Withdrawal",
|
label: "Withdrawal",
|
||||||
options: "currency",
|
options: "account_currency",
|
||||||
|
read_only: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "column_break_17",
|
||||||
|
fieldtype: "Column Break",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -386,28 +412,22 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
|||||||
label: "Description",
|
label: "Description",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fieldname: "column_break_17",
|
|
||||||
fieldtype: "Column Break",
|
|
||||||
read_only: 1,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldname: "allocated_amount",
|
fieldname: "allocated_amount",
|
||||||
fieldtype: "Currency",
|
fieldtype: "Currency",
|
||||||
label: "Allocated Amount",
|
label: "Allocated Amount",
|
||||||
options: "Currency",
|
options: "account_currency",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
fieldname: "unallocated_amount",
|
fieldname: "unallocated_amount",
|
||||||
fieldtype: "Currency",
|
fieldtype: "Currency",
|
||||||
label: "Unallocated Amount",
|
label: "Unallocated Amount",
|
||||||
options: "Currency",
|
options: "account_currency",
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "currency",
|
fieldname: "account_currency",
|
||||||
fieldtype: "Link",
|
fieldtype: "Link",
|
||||||
label: "Currency",
|
label: "Currency",
|
||||||
options: "Currency",
|
options: "Currency",
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_calculate_taxes_and_totals() {
|
_calculate_taxes_and_totals() {
|
||||||
|
const is_quotation = this.frm.doc.doctype == "Quotation";
|
||||||
|
this.frm.doc._items = is_quotation ? this.filtered_items() : this.frm.doc.items;
|
||||||
|
|
||||||
this.validate_conversion_rate();
|
this.validate_conversion_rate();
|
||||||
this.calculate_item_values();
|
this.calculate_item_values();
|
||||||
this.initialize_taxes();
|
this.initialize_taxes();
|
||||||
@@ -122,7 +125,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
calculate_item_values() {
|
calculate_item_values() {
|
||||||
var me = this;
|
var me = this;
|
||||||
if (!this.discount_amount_applied) {
|
if (!this.discount_amount_applied) {
|
||||||
for (const item of this.frm.doc.items || []) {
|
for (const item of this.frm.doc._items || []) {
|
||||||
frappe.model.round_floats_in(item);
|
frappe.model.round_floats_in(item);
|
||||||
item.net_rate = item.rate;
|
item.net_rate = item.rate;
|
||||||
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
|
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
|
||||||
@@ -132,7 +135,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// allow for '0' qty on Credit/Debit notes
|
// allow for '0' qty on Credit/Debit notes
|
||||||
let qty = item.qty || -1
|
let qty = item.qty || me.frm.doc.is_debit_note ? 1 : -1;
|
||||||
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
|
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +209,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
});
|
});
|
||||||
if(has_inclusive_tax==false) return;
|
if(has_inclusive_tax==false) return;
|
||||||
|
|
||||||
$.each(me.frm.doc["items"] || [], function(n, item) {
|
$.each(me.frm.doc._items || [], function(n, item) {
|
||||||
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
||||||
var cumulated_tax_fraction = 0.0;
|
var cumulated_tax_fraction = 0.0;
|
||||||
var total_inclusive_tax_amount_per_qty = 0;
|
var total_inclusive_tax_amount_per_qty = 0;
|
||||||
@@ -277,7 +280,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
var me = this;
|
var me = this;
|
||||||
this.frm.doc.total_qty = this.frm.doc.total = this.frm.doc.base_total = this.frm.doc.net_total = this.frm.doc.base_net_total = 0.0;
|
this.frm.doc.total_qty = this.frm.doc.total = this.frm.doc.base_total = this.frm.doc.net_total = this.frm.doc.base_net_total = 0.0;
|
||||||
|
|
||||||
$.each(this.frm.doc["items"] || [], function(i, item) {
|
$.each(this.frm.doc._items || [], function(i, item) {
|
||||||
me.frm.doc.total += item.amount;
|
me.frm.doc.total += item.amount;
|
||||||
me.frm.doc.total_qty += item.qty;
|
me.frm.doc.total_qty += item.qty;
|
||||||
me.frm.doc.base_total += item.base_amount;
|
me.frm.doc.base_total += item.base_amount;
|
||||||
@@ -330,7 +333,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$.each(this.frm.doc["items"] || [], function(n, item) {
|
$.each(this.frm.doc._items || [], function(n, item) {
|
||||||
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
||||||
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
|
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
|
||||||
// tax_amount represents the amount of tax for the current step
|
// tax_amount represents the amount of tax for the current step
|
||||||
@@ -339,7 +342,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
// Adjust divisional loss to the last item
|
// Adjust divisional loss to the last item
|
||||||
if (tax.charge_type == "Actual") {
|
if (tax.charge_type == "Actual") {
|
||||||
actual_tax_dict[tax.idx] -= current_tax_amount;
|
actual_tax_dict[tax.idx] -= current_tax_amount;
|
||||||
if (n == me.frm.doc["items"].length - 1) {
|
if (n == me.frm.doc._items.length - 1) {
|
||||||
current_tax_amount += actual_tax_dict[tax.idx];
|
current_tax_amount += actual_tax_dict[tax.idx];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,7 +379,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set precision in the last item iteration
|
// set precision in the last item iteration
|
||||||
if (n == me.frm.doc["items"].length - 1) {
|
if (n == me.frm.doc._items.length - 1) {
|
||||||
me.round_off_totals(tax);
|
me.round_off_totals(tax);
|
||||||
me.set_in_company_currency(tax,
|
me.set_in_company_currency(tax,
|
||||||
["tax_amount", "tax_amount_after_discount_amount"]);
|
["tax_amount", "tax_amount_after_discount_amount"]);
|
||||||
@@ -599,10 +602,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
|
|
||||||
_cleanup() {
|
_cleanup() {
|
||||||
this.frm.doc.base_in_words = this.frm.doc.in_words = "";
|
this.frm.doc.base_in_words = this.frm.doc.in_words = "";
|
||||||
|
let items = this.frm.doc._items;
|
||||||
|
|
||||||
if(this.frm.doc["items"] && this.frm.doc["items"].length) {
|
if(items && items.length) {
|
||||||
if(!frappe.meta.get_docfield(this.frm.doc["items"][0].doctype, "item_tax_amount", this.frm.doctype)) {
|
if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {
|
||||||
$.each(this.frm.doc["items"] || [], function(i, item) {
|
$.each(items || [], function(i, item) {
|
||||||
delete item["item_tax_amount"];
|
delete item["item_tax_amount"];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -655,7 +659,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
var net_total = 0;
|
var net_total = 0;
|
||||||
// calculate item amount after Discount Amount
|
// calculate item amount after Discount Amount
|
||||||
if (total_for_discount_amount) {
|
if (total_for_discount_amount) {
|
||||||
$.each(this.frm.doc["items"] || [], function(i, item) {
|
$.each(this.frm.doc._items || [], function(i, item) {
|
||||||
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
|
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
|
||||||
item.net_amount = flt(item.net_amount - distributed_amount,
|
item.net_amount = flt(item.net_amount - distributed_amount,
|
||||||
precision("base_amount", item));
|
precision("base_amount", item));
|
||||||
@@ -663,7 +667,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
|
|
||||||
// discount amount rounding loss adjustment if no taxes
|
// discount amount rounding loss adjustment if no taxes
|
||||||
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
|
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
|
||||||
&& i == (me.frm.doc.items || []).length - 1) {
|
&& i == (me.frm.doc._items || []).length - 1) {
|
||||||
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
|
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
|
||||||
- me.frm.doc.discount_amount, precision("net_total"));
|
- me.frm.doc.discount_amount, precision("net_total"));
|
||||||
item.net_amount = flt(item.net_amount + discount_amount_loss,
|
item.net_amount = flt(item.net_amount + discount_amount_loss,
|
||||||
@@ -892,4 +896,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filtered_items() {
|
||||||
|
return this.frm.doc.items.filter(item => !item["is_alternative"]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
|| frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
|
|| frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__("Sales Order"),
|
__("Sales Order"),
|
||||||
this.frm.cscript["Make Sales Order"],
|
() => this.make_sales_order(),
|
||||||
__("Create")
|
__("Create")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -145,6 +145,20 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
make_sales_order() {
|
||||||
|
var me = this;
|
||||||
|
|
||||||
|
let has_alternative_item = this.frm.doc.items.some((item) => item.is_alternative);
|
||||||
|
if (has_alternative_item) {
|
||||||
|
this.show_alternative_items_dialog();
|
||||||
|
} else {
|
||||||
|
frappe.model.open_mapped_doc({
|
||||||
|
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
||||||
|
frm: me.frm
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
set_dynamic_field_label(){
|
set_dynamic_field_label(){
|
||||||
if (this.frm.doc.quotation_to == "Customer")
|
if (this.frm.doc.quotation_to == "Customer")
|
||||||
{
|
{
|
||||||
@@ -220,17 +234,111 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show_alternative_items_dialog() {
|
||||||
|
let me = this;
|
||||||
|
|
||||||
|
const table_fields = [
|
||||||
|
{
|
||||||
|
fieldtype:"Data",
|
||||||
|
fieldname:"name",
|
||||||
|
label: __("Name"),
|
||||||
|
read_only: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype:"Link",
|
||||||
|
fieldname:"item_code",
|
||||||
|
options: "Item",
|
||||||
|
label: __("Item Code"),
|
||||||
|
read_only: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
columns: 2,
|
||||||
|
formatter: (value, df, options, doc) => {
|
||||||
|
return doc.is_alternative ? `<span class="indicator yellow">${value}</span>` : value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype:"Data",
|
||||||
|
fieldname:"description",
|
||||||
|
label: __("Description"),
|
||||||
|
in_list_view: 1,
|
||||||
|
read_only: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype:"Currency",
|
||||||
|
fieldname:"amount",
|
||||||
|
label: __("Amount"),
|
||||||
|
options: "currency",
|
||||||
|
in_list_view: 1,
|
||||||
|
read_only: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype:"Check",
|
||||||
|
fieldname:"is_alternative",
|
||||||
|
label: __("Is Alternative"),
|
||||||
|
read_only: 1,
|
||||||
|
}];
|
||||||
|
|
||||||
|
|
||||||
|
this.data = this.frm.doc.items.filter(
|
||||||
|
(item) => item.is_alternative || item.has_alternative_item
|
||||||
|
).map((item) => {
|
||||||
|
return {
|
||||||
|
"name": item.name,
|
||||||
|
"item_code": item.item_code,
|
||||||
|
"description": item.description,
|
||||||
|
"amount": item.amount,
|
||||||
|
"is_alternative": item.is_alternative,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialog = new frappe.ui.Dialog({
|
||||||
|
title: __("Select Alternative Items for Sales Order"),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: "info",
|
||||||
|
fieldtype: "HTML",
|
||||||
|
read_only: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "alternative_items",
|
||||||
|
fieldtype: "Table",
|
||||||
|
cannot_add_rows: true,
|
||||||
|
in_place_edit: true,
|
||||||
|
reqd: 1,
|
||||||
|
data: this.data,
|
||||||
|
description: __("Select an item from each set to be used in the Sales Order."),
|
||||||
|
get_data: () => {
|
||||||
|
return this.data;
|
||||||
|
},
|
||||||
|
fields: table_fields
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary_action: function() {
|
||||||
|
frappe.model.open_mapped_doc({
|
||||||
|
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
||||||
|
frm: me.frm,
|
||||||
|
args: {
|
||||||
|
selected_items: dialog.fields_dict.alternative_items.grid.get_selected_children()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog.hide();
|
||||||
|
},
|
||||||
|
primary_action_label: __('Continue')
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.fields_dict.info.$wrapper.html(
|
||||||
|
`<p class="small text-muted">
|
||||||
|
<span class="indicator yellow"></span>
|
||||||
|
Alternative Items
|
||||||
|
</p>`
|
||||||
|
)
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
cur_frm.script_manager.make(erpnext.selling.QuotationController);
|
cur_frm.script_manager.make(erpnext.selling.QuotationController);
|
||||||
|
|
||||||
cur_frm.cscript['Make Sales Order'] = function() {
|
|
||||||
frappe.model.open_mapped_doc({
|
|
||||||
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
|
||||||
frm: cur_frm
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.ui.form.on("Quotation Item", "items_on_form_rendered", "packed_items_on_form_rendered", function(frm, cdt, cdn) {
|
frappe.ui.form.on("Quotation Item", "items_on_form_rendered", "packed_items_on_form_rendered", function(frm, cdt, cdn) {
|
||||||
// enable tax_amount field if Actual
|
// enable tax_amount field if Actual
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class Quotation(SellingController):
|
|||||||
|
|
||||||
make_packing_list(self)
|
make_packing_list(self)
|
||||||
|
|
||||||
|
def before_submit(self):
|
||||||
|
self.set_has_alternative_item()
|
||||||
|
|
||||||
def validate_valid_till(self):
|
def validate_valid_till(self):
|
||||||
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
|
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
|
||||||
frappe.throw(_("Valid till date cannot be before transaction date"))
|
frappe.throw(_("Valid till date cannot be before transaction date"))
|
||||||
@@ -59,7 +62,18 @@ class Quotation(SellingController):
|
|||||||
title=_("Unpublished Item"),
|
title=_("Unpublished Item"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_has_alternative_item(self):
|
||||||
|
"""Mark 'Has Alternative Item' for rows."""
|
||||||
|
if not any(row.is_alternative for row in self.get("items")):
|
||||||
|
return
|
||||||
|
|
||||||
|
items_with_alternatives = self.get_rows_with_alternatives()
|
||||||
|
for row in self.get("items"):
|
||||||
|
if not row.is_alternative and row.name in items_with_alternatives:
|
||||||
|
row.has_alternative_item = 1
|
||||||
|
|
||||||
def get_ordered_status(self):
|
def get_ordered_status(self):
|
||||||
|
status = "Open"
|
||||||
ordered_items = frappe._dict(
|
ordered_items = frappe._dict(
|
||||||
frappe.db.get_all(
|
frappe.db.get_all(
|
||||||
"Sales Order Item",
|
"Sales Order Item",
|
||||||
@@ -70,16 +84,40 @@ class Quotation(SellingController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
status = "Open"
|
if not ordered_items:
|
||||||
if ordered_items:
|
return status
|
||||||
|
|
||||||
|
has_alternatives = any(row.is_alternative for row in self.get("items"))
|
||||||
|
self._items = self.get_valid_items() if has_alternatives else self.get("items")
|
||||||
|
|
||||||
|
if any(row.qty > ordered_items.get(row.item_code, 0.0) for row in self._items):
|
||||||
|
status = "Partially Ordered"
|
||||||
|
else:
|
||||||
status = "Ordered"
|
status = "Ordered"
|
||||||
|
|
||||||
for item in self.get("items"):
|
|
||||||
if item.qty > ordered_items.get(item.item_code, 0.0):
|
|
||||||
status = "Partially Ordered"
|
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
def get_valid_items(self):
|
||||||
|
"""
|
||||||
|
Filters out items in an alternatives set that were not ordered.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def is_in_sales_order(row):
|
||||||
|
in_sales_order = bool(
|
||||||
|
frappe.db.exists(
|
||||||
|
"Sales Order Item", {"quotation_item": row.name, "item_code": row.item_code, "docstatus": 1}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return in_sales_order
|
||||||
|
|
||||||
|
def can_map(row) -> bool:
|
||||||
|
if row.is_alternative or row.has_alternative_item:
|
||||||
|
return is_in_sales_order(row)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
return list(filter(can_map, self.get("items")))
|
||||||
|
|
||||||
def is_fully_ordered(self):
|
def is_fully_ordered(self):
|
||||||
return self.get_ordered_status() == "Ordered"
|
return self.get_ordered_status() == "Ordered"
|
||||||
|
|
||||||
@@ -176,6 +214,22 @@ class Quotation(SellingController):
|
|||||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||||
self.valid_till = None
|
self.valid_till = None
|
||||||
|
|
||||||
|
def get_rows_with_alternatives(self):
|
||||||
|
rows_with_alternatives = []
|
||||||
|
table_length = len(self.get("items"))
|
||||||
|
|
||||||
|
for idx, row in enumerate(self.get("items")):
|
||||||
|
if row.is_alternative:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if idx == (table_length - 1):
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.get("items")[idx + 1].is_alternative:
|
||||||
|
rows_with_alternatives.append(row.name)
|
||||||
|
|
||||||
|
return rows_with_alternatives
|
||||||
|
|
||||||
|
|
||||||
def get_list_context(context=None):
|
def get_list_context(context=None):
|
||||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||||
@@ -221,6 +275,8 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])]
|
||||||
|
|
||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
if customer:
|
if customer:
|
||||||
target.customer = customer.name
|
target.customer = customer.name
|
||||||
@@ -244,6 +300,24 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
target.blanket_order = obj.blanket_order
|
target.blanket_order = obj.blanket_order
|
||||||
target.blanket_order_rate = obj.blanket_order_rate
|
target.blanket_order_rate = obj.blanket_order_rate
|
||||||
|
|
||||||
|
def can_map_row(item) -> bool:
|
||||||
|
"""
|
||||||
|
Row mapping from Quotation to Sales order:
|
||||||
|
1. If no selections, map all non-alternative rows (that sum up to the grand total)
|
||||||
|
2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty
|
||||||
|
3. If selections: Simple row: Map if adequate qty
|
||||||
|
"""
|
||||||
|
has_qty = item.qty > 0
|
||||||
|
|
||||||
|
if not selected_rows:
|
||||||
|
return not item.is_alternative
|
||||||
|
|
||||||
|
if selected_rows and (item.is_alternative or item.has_alternative_item):
|
||||||
|
return (item.name in selected_rows) and has_qty
|
||||||
|
|
||||||
|
# Simple row
|
||||||
|
return has_qty
|
||||||
|
|
||||||
doclist = get_mapped_doc(
|
doclist = get_mapped_doc(
|
||||||
"Quotation",
|
"Quotation",
|
||||||
source_name,
|
source_name,
|
||||||
@@ -253,7 +327,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
"doctype": "Sales Order Item",
|
"doctype": "Sales Order Item",
|
||||||
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
|
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: doc.qty > 0,
|
"condition": can_map_row,
|
||||||
},
|
},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
||||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||||
@@ -322,7 +396,11 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
source_name,
|
source_name,
|
||||||
{
|
{
|
||||||
"Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}},
|
"Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}},
|
||||||
"Quotation Item": {"doctype": "Sales Invoice Item", "postprocess": update_item},
|
"Quotation Item": {
|
||||||
|
"doctype": "Sales Invoice Item",
|
||||||
|
"postprocess": update_item,
|
||||||
|
"condition": lambda row: not row.is_alternative,
|
||||||
|
},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
||||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -457,6 +457,139 @@ class TestQuotation(FrappeTestCase):
|
|||||||
expected_index = id + 1
|
expected_index = id + 1
|
||||||
self.assertEqual(item.idx, expected_index)
|
self.assertEqual(item.idx, expected_index)
|
||||||
|
|
||||||
|
def test_alternative_items_with_stock_items(self):
|
||||||
|
"""
|
||||||
|
Check if taxes & totals considers only non-alternative items with:
|
||||||
|
- One set of non-alternative & alternative items [first 3 rows]
|
||||||
|
- One simple stock item
|
||||||
|
"""
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
item_list = []
|
||||||
|
stock_items = {
|
||||||
|
"_Test Simple Item 1": 100,
|
||||||
|
"_Test Alt 1": 120,
|
||||||
|
"_Test Alt 2": 110,
|
||||||
|
"_Test Simple Item 2": 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
for item, rate in stock_items.items():
|
||||||
|
make_item(item, {"is_stock_item": 1})
|
||||||
|
item_list.append(
|
||||||
|
{
|
||||||
|
"item_code": item,
|
||||||
|
"qty": 1,
|
||||||
|
"rate": rate,
|
||||||
|
"is_alternative": bool("Alt" in item),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
quotation = make_quotation(item_list=item_list, do_not_submit=1)
|
||||||
|
quotation.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"rate": 10,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
quotation.submit()
|
||||||
|
|
||||||
|
self.assertEqual(quotation.net_total, 300)
|
||||||
|
self.assertEqual(quotation.grand_total, 330)
|
||||||
|
|
||||||
|
def test_alternative_items_with_service_items(self):
|
||||||
|
"""
|
||||||
|
Check if taxes & totals considers only non-alternative items with:
|
||||||
|
- One set of non-alternative & alternative service items [first 3 rows]
|
||||||
|
- One simple non-alternative service item
|
||||||
|
All having the same item code and unique item name/description due to
|
||||||
|
dynamic services
|
||||||
|
"""
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
item_list = []
|
||||||
|
service_items = {
|
||||||
|
"Tiling with Standard Tiles": 100,
|
||||||
|
"Alt Tiling with Durable Tiles": 150,
|
||||||
|
"Alt Tiling with Premium Tiles": 180,
|
||||||
|
"False Ceiling with Material #234": 190,
|
||||||
|
}
|
||||||
|
|
||||||
|
make_item("_Test Dynamic Service Item", {"is_stock_item": 0})
|
||||||
|
|
||||||
|
for name, rate in service_items.items():
|
||||||
|
item_list.append(
|
||||||
|
{
|
||||||
|
"item_code": "_Test Dynamic Service Item",
|
||||||
|
"item_name": name,
|
||||||
|
"description": name,
|
||||||
|
"qty": 1,
|
||||||
|
"rate": rate,
|
||||||
|
"is_alternative": bool("Alt" in name),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
quotation = make_quotation(item_list=item_list, do_not_submit=1)
|
||||||
|
quotation.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"rate": 10,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
quotation.submit()
|
||||||
|
|
||||||
|
self.assertEqual(quotation.net_total, 290)
|
||||||
|
self.assertEqual(quotation.grand_total, 319)
|
||||||
|
|
||||||
|
def test_alternative_items_sales_order_mapping_with_stock_items(self):
|
||||||
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
frappe.flags.args = frappe._dict()
|
||||||
|
item_list = []
|
||||||
|
stock_items = {
|
||||||
|
"_Test Simple Item 1": 100,
|
||||||
|
"_Test Alt 1": 120,
|
||||||
|
"_Test Alt 2": 110,
|
||||||
|
"_Test Simple Item 2": 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
for item, rate in stock_items.items():
|
||||||
|
make_item(item, {"is_stock_item": 1})
|
||||||
|
item_list.append(
|
||||||
|
{
|
||||||
|
"item_code": item,
|
||||||
|
"qty": 1,
|
||||||
|
"rate": rate,
|
||||||
|
"is_alternative": bool("Alt" in item),
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
quotation = make_quotation(item_list=item_list)
|
||||||
|
|
||||||
|
frappe.flags.args.selected_items = [quotation.items[2]]
|
||||||
|
sales_order = make_sales_order(quotation.name)
|
||||||
|
sales_order.delivery_date = add_days(sales_order.transaction_date, 10)
|
||||||
|
sales_order.save()
|
||||||
|
|
||||||
|
self.assertEqual(sales_order.items[0].item_code, "_Test Alt 2")
|
||||||
|
self.assertEqual(sales_order.items[1].item_code, "_Test Simple Item 2")
|
||||||
|
self.assertEqual(sales_order.net_total, 310)
|
||||||
|
|
||||||
|
sales_order.submit()
|
||||||
|
quotation.reload()
|
||||||
|
self.assertEqual(quotation.status, "Ordered")
|
||||||
|
|
||||||
|
|
||||||
test_records = frappe.get_test_records("Quotation")
|
test_records = frappe.get_test_records("Quotation")
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,8 @@
|
|||||||
"pricing_rules",
|
"pricing_rules",
|
||||||
"stock_uom_rate",
|
"stock_uom_rate",
|
||||||
"is_free_item",
|
"is_free_item",
|
||||||
|
"is_alternative",
|
||||||
|
"has_alternative_item",
|
||||||
"section_break_43",
|
"section_break_43",
|
||||||
"valuation_rate",
|
"valuation_rate",
|
||||||
"column_break_45",
|
"column_break_45",
|
||||||
@@ -644,12 +646,28 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_alternative",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Alternative",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "has_alternative_item",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Has Alternative Item",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-07-15 12:40:51.074820",
|
"modified": "2023-02-06 11:00:07.042364",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Quotation Item",
|
"name": "Quotation Item",
|
||||||
@@ -657,5 +675,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -275,7 +275,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
if (this.frm.doc.docstatus===0) {
|
if (this.frm.doc.docstatus===0) {
|
||||||
this.frm.add_custom_button(__('Quotation'),
|
this.frm.add_custom_button(__('Quotation'),
|
||||||
function() {
|
function() {
|
||||||
erpnext.utils.map_current_doc({
|
let d = erpnext.utils.map_current_doc({
|
||||||
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
||||||
source_doctype: "Quotation",
|
source_doctype: "Quotation",
|
||||||
target: me.frm,
|
target: me.frm,
|
||||||
@@ -293,7 +293,16 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
docstatus: 1,
|
docstatus: 1,
|
||||||
status: ["!=", "Lost"]
|
status: ["!=", "Lost"]
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
d.$parent.append(`
|
||||||
|
<span class='small text-muted'>
|
||||||
|
${__("Note: Please create Sales Orders from individual Quotations to select from among Alternative Items.")}
|
||||||
|
</span>
|
||||||
|
`);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
}, __("Get Items From"));
|
}, __("Get Items From"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
|||||||
)
|
)
|
||||||
from erpnext.accounts.party import get_party_account
|
from erpnext.accounts.party import get_party_account
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
|
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||||
|
validate_against_blanket_order,
|
||||||
|
)
|
||||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||||
get_items_for_material_requests,
|
get_items_for_material_requests,
|
||||||
)
|
)
|
||||||
@@ -52,6 +55,7 @@ class SalesOrder(SellingController):
|
|||||||
self.validate_warehouse()
|
self.validate_warehouse()
|
||||||
self.validate_drop_ship()
|
self.validate_drop_ship()
|
||||||
self.validate_serial_no_based_delivery()
|
self.validate_serial_no_based_delivery()
|
||||||
|
validate_against_blanket_order(self)
|
||||||
validate_inter_company_party(
|
validate_inter_company_party(
|
||||||
self.doctype, self.customer, self.company, self.inter_company_order_reference
|
self.doctype, self.customer, self.company, self.inter_company_order_reference
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"so_required",
|
"so_required",
|
||||||
"dn_required",
|
"dn_required",
|
||||||
"sales_update_frequency",
|
"sales_update_frequency",
|
||||||
|
"over_order_allowance",
|
||||||
"column_break_5",
|
"column_break_5",
|
||||||
"allow_multiple_items",
|
"allow_multiple_items",
|
||||||
"allow_against_multiple_purchase_orders",
|
"allow_against_multiple_purchase_orders",
|
||||||
@@ -179,6 +180,12 @@
|
|||||||
"fieldname": "allow_sales_order_creation_for_expired_quotation",
|
"fieldname": "allow_sales_order_creation_for_expired_quotation",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Sales Order Creation For Expired Quotation"
|
"label": "Allow Sales Order Creation For Expired Quotation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
|
||||||
|
"fieldname": "over_order_allowance",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Over Order Allowance (%)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
@@ -186,7 +193,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-02-04 12:37:53.380857",
|
"modified": "2023-03-03 11:16:54.333615",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Selling Settings",
|
"name": "Selling Settings",
|
||||||
|
|||||||
@@ -808,7 +808,7 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad
|
|||||||
return existing_address
|
return existing_address
|
||||||
|
|
||||||
if out:
|
if out:
|
||||||
return min(out, key=lambda x: x[1])[0] # find min by sort_key
|
return max(out, key=lambda x: x[1])[0] # find max by sort_key
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from frappe.utils import random_string
|
|||||||
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import (
|
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import (
|
||||||
get_charts_for_country,
|
get_charts_for_country,
|
||||||
)
|
)
|
||||||
|
from erpnext.setup.doctype.company.company import get_default_company_address
|
||||||
|
|
||||||
test_ignore = ["Account", "Cost Center", "Payment Terms Template", "Salary Component", "Warehouse"]
|
test_ignore = ["Account", "Cost Center", "Payment Terms Template", "Salary Component", "Warehouse"]
|
||||||
test_dependencies = ["Fiscal Year"]
|
test_dependencies = ["Fiscal Year"]
|
||||||
@@ -132,6 +133,38 @@ class TestCompany(unittest.TestCase):
|
|||||||
self.assertTrue(lft >= min_lft)
|
self.assertTrue(lft >= min_lft)
|
||||||
self.assertTrue(rgt <= max_rgt)
|
self.assertTrue(rgt <= max_rgt)
|
||||||
|
|
||||||
|
def test_primary_address(self):
|
||||||
|
company = "_Test Company"
|
||||||
|
|
||||||
|
secondary = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"address_title": "Non Primary",
|
||||||
|
"doctype": "Address",
|
||||||
|
"address_type": "Billing",
|
||||||
|
"address_line1": "Something",
|
||||||
|
"city": "Mumbai",
|
||||||
|
"state": "Maharashtra",
|
||||||
|
"country": "India",
|
||||||
|
"is_primary_address": 1,
|
||||||
|
"pincode": "400098",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"link_doctype": "Company",
|
||||||
|
"link_name": company,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
secondary.insert()
|
||||||
|
self.addCleanup(secondary.delete)
|
||||||
|
|
||||||
|
primary = frappe.copy_doc(secondary)
|
||||||
|
primary.is_primary_address = 1
|
||||||
|
primary.insert()
|
||||||
|
self.addCleanup(primary.delete)
|
||||||
|
|
||||||
|
self.assertEqual(get_default_company_address(company), primary.name)
|
||||||
|
|
||||||
def get_no_of_children(self, company):
|
def get_no_of_children(self, company):
|
||||||
def get_no_of_children(companies, no_of_children):
|
def get_no_of_children(companies, no_of_children):
|
||||||
children = []
|
children = []
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ def add_standard_navbar_items():
|
|||||||
{
|
{
|
||||||
"item_label": "Documentation",
|
"item_label": "Documentation",
|
||||||
"item_type": "Route",
|
"item_type": "Route",
|
||||||
"route": "https://erpnext.com/docs/user/manual",
|
"route": "https://docs.erpnext.com/docs/v14/user/manual/en/introduction",
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -636,7 +636,8 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Sales Invoice",
|
"options": "Sales Invoice",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "so_detail",
|
"fieldname": "so_detail",
|
||||||
@@ -837,7 +838,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-09 12:17:50.850142",
|
"modified": "2023-03-20 14:24:10.406746",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note Item",
|
"name": "Delivery Note Item",
|
||||||
|
|||||||
@@ -661,6 +661,7 @@ class StockEntry(StockController):
|
|||||||
)
|
)
|
||||||
finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
|
finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
|
||||||
|
|
||||||
|
items = []
|
||||||
# Set basic rate for incoming items
|
# Set basic rate for incoming items
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.s_warehouse or d.set_basic_rate_manually:
|
if d.s_warehouse or d.set_basic_rate_manually:
|
||||||
@@ -668,12 +669,7 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
if d.allow_zero_valuation_rate:
|
if d.allow_zero_valuation_rate:
|
||||||
d.basic_rate = 0.0
|
d.basic_rate = 0.0
|
||||||
frappe.msgprint(
|
items.append(d.item_code)
|
||||||
_(
|
|
||||||
"Row {0}: Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {1}"
|
|
||||||
).format(d.idx, d.item_code),
|
|
||||||
alert=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif d.is_finished_item:
|
elif d.is_finished_item:
|
||||||
if self.purpose == "Manufacture":
|
if self.purpose == "Manufacture":
|
||||||
@@ -700,6 +696,20 @@ class StockEntry(StockController):
|
|||||||
d.basic_rate = flt(d.basic_rate)
|
d.basic_rate = flt(d.basic_rate)
|
||||||
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
|
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
|
||||||
|
|
||||||
|
if items:
|
||||||
|
message = ""
|
||||||
|
|
||||||
|
if len(items) > 1:
|
||||||
|
message = _(
|
||||||
|
"Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}"
|
||||||
|
).format(", ".join(frappe.bold(item) for item in items))
|
||||||
|
else:
|
||||||
|
message = _(
|
||||||
|
"Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}"
|
||||||
|
).format(frappe.bold(items[0]))
|
||||||
|
|
||||||
|
frappe.msgprint(message, alert=True)
|
||||||
|
|
||||||
def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
|
def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
|
||||||
outgoing_items_cost = 0.0
|
outgoing_items_cost = 0.0
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
|
|||||||
@@ -186,14 +186,14 @@ class ItemConfigure {
|
|||||||
this.dialog.$status_area.empty();
|
this.dialog.$status_area.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info }) {
|
get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info, available_qty, settings }) {
|
||||||
const one_item = exact_match.length === 1
|
const one_item = exact_match.length === 1
|
||||||
? exact_match[0]
|
? exact_match[0]
|
||||||
: filtered_items_count === 1
|
: filtered_items_count === 1
|
||||||
? filtered_items[0]
|
? filtered_items[0]
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const item_add_to_cart = one_item ? `
|
let item_add_to_cart = one_item ? `
|
||||||
<button data-item-code="${one_item}"
|
<button data-item-code="${one_item}"
|
||||||
class="btn btn-primary btn-add-to-cart w-100"
|
class="btn btn-primary btn-add-to-cart w-100"
|
||||||
data-action="btn_add_to_cart"
|
data-action="btn_add_to_cart"
|
||||||
@@ -218,6 +218,9 @@ class ItemConfigure {
|
|||||||
? '(' + product_info.price.formatted_price_sales_uom + ')'
|
? '(' + product_info.price.formatted_price_sales_uom + ')'
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${available_qty === 0 ? '<span class="text-danger">(' + __('Out of Stock') + ')</span>' : ''}
|
||||||
|
|
||||||
</div></div>
|
</div></div>
|
||||||
<a href data-action="btn_clear_values" data-item-code="${one_item}">
|
<a href data-action="btn_clear_values" data-item-code="${one_item}">
|
||||||
${__('Clear Values')}
|
${__('Clear Values')}
|
||||||
@@ -233,6 +236,10 @@ class ItemConfigure {
|
|||||||
</div>`;
|
</div>`;
|
||||||
/* eslint-disable indent */
|
/* eslint-disable indent */
|
||||||
|
|
||||||
|
if (!product_info?.allow_items_not_in_stock && available_qty === 0) {
|
||||||
|
item_add_to_cart = '';
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${item_found_status}
|
${item_found_status}
|
||||||
${item_add_to_cart}
|
${item_add_to_cart}
|
||||||
@@ -257,12 +264,15 @@ class ItemConfigure {
|
|||||||
|
|
||||||
btn_clear_values() {
|
btn_clear_values() {
|
||||||
this.dialog.fields_list.forEach(f => {
|
this.dialog.fields_list.forEach(f => {
|
||||||
f.df.options = f.df.options.map(option => {
|
if (f.df?.options) {
|
||||||
option.disabled = false;
|
f.df.options = f.df.options.map(option => {
|
||||||
return option;
|
option.disabled = false;
|
||||||
});
|
return option;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.dialog.clear();
|
this.dialog.clear();
|
||||||
|
this.dialog.$status_area.empty();
|
||||||
this.on_attribute_selection();
|
this.on_attribute_selection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2012,30 +2012,27 @@ Please identify/create Account (Ledger) for type - {0},Bitte identifizieren / er
|
|||||||
Please login as another user to register on Marketplace,"Bitte melden Sie sich als anderer Benutzer an, um sich auf dem Marktplatz zu registrieren",
|
Please login as another user to register on Marketplace,"Bitte melden Sie sich als anderer Benutzer an, um sich auf dem Marktplatz zu registrieren",
|
||||||
Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.,"Bitte sicher stellen, dass wirklich alle Transaktionen dieses Unternehmens gelöscht werden sollen. Die Stammdaten bleiben bestehen. Diese Aktion kann nicht rückgängig gemacht werden.",
|
Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.,"Bitte sicher stellen, dass wirklich alle Transaktionen dieses Unternehmens gelöscht werden sollen. Die Stammdaten bleiben bestehen. Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
Please mention Basic and HRA component in Company,Bitte erwähnen Sie die Basis- und HRA-Komponente in der Firma,
|
Please mention Basic and HRA component in Company,Bitte erwähnen Sie die Basis- und HRA-Komponente in der Firma,
|
||||||
Please mention Round Off Account in Company,Bitte Abschlusskonto in Unternehmen vermerken,
|
Please mention Round Off Account in Company,Bitte ein Standardkonto Konto für Rundungsdifferenzen in Unternehmen einstellen,
|
||||||
Please mention Round Off Cost Center in Company,Bitte Abschlusskostenstelle in Unternehmen vermerken,
|
Please mention Round Off Cost Center in Company,Bitte eine Kostenstelle für Rundungsdifferenzen in Unternehmen einstellen,
|
||||||
Please mention no of visits required,"Bitte bei ""Besuche erforderlich"" NEIN angeben",
|
Please mention no of visits required,Bitte die Anzahl der benötigten Wartungsbesuche angeben,
|
||||||
Please mention the Lead Name in Lead {0},Bitte erwähnen Sie den Lead Name in Lead {0},
|
Please pull items from Delivery Note,Bitte Artikel aus dem Lieferschein ziehen,
|
||||||
Please pull items from Delivery Note,Bitte Artikel vom Lieferschein nehmen,
|
|
||||||
Please register the SIREN number in the company information file,Bitte registrieren Sie die SIREN-Nummer in der Unternehmensinformationsdatei,
|
Please register the SIREN number in the company information file,Bitte registrieren Sie die SIREN-Nummer in der Unternehmensinformationsdatei,
|
||||||
Please remove this Invoice {0} from C-Form {1},Bitte diese Rechnung {0} vom Kontaktformular {1} entfernen,
|
Please remove this Invoice {0} from C-Form {1},Bitte diese Rechnung {0} vom Kontaktformular {1} entfernen,
|
||||||
Please save the patient first,Bitte speichern Sie den Patienten zuerst,
|
Please save the patient first,Bitte speichern Sie den Patienten zuerst,
|
||||||
Please save the report again to rebuild or update,"Speichern Sie den Bericht erneut, um ihn neu zu erstellen oder zu aktualisieren",
|
Please save the report again to rebuild or update,"Speichern Sie den Bericht erneut, um ihn neu zu erstellen oder zu aktualisieren",
|
||||||
"Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row","Bitte zugewiesenen Betrag, Rechnungsart und Rechnungsnummer in mindestens einer Zeile auswählen",
|
"Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row","Bitte zugewiesenen Betrag, Rechnungsart und Rechnungsnummer in mindestens einer Zeile auswählen",
|
||||||
Please select Apply Discount On,"Bitte ""Rabatt anwenden auf"" auswählen",
|
Please select Apply Discount On,"Bitte ""Rabatt anwenden auf"" auswählen",
|
||||||
Please select BOM against item {0},Bitte wählen Sie Stückliste gegen Artikel {0},
|
Please select BOM against item {0},Bitte eine Stückliste für Artikel {0} auswählen,
|
||||||
Please select BOM for Item in Row {0},Bitte Stückliste für Artikel in Zeile {0} auswählen,
|
Please select BOM for Item in Row {0},Bitte eine Stückliste für den Artikel in Zeile {0} auswählen,
|
||||||
Please select BOM in BOM field for Item {0},Bitte aus dem Stücklistenfeld eine Stückliste für Artikel {0} auswählen,
|
Please select BOM in BOM field for Item {0},Bitte im Stücklistenfeld eine Stückliste für Artikel {0} auswählen,
|
||||||
Please select Category first,Bitte zuerst Kategorie auswählen,
|
Please select Category first,Bitte zuerst eine Kategorie auswählen,
|
||||||
Please select Charge Type first,Bitte zuerst Chargentyp auswählen,
|
Please select Charge Type first,Bitte zuerst einen Chargentyp auswählen,
|
||||||
Please select Company,Bitte Unternehmen auswählen,
|
Please select Company,Bitte ein Unternehmen auswählen,
|
||||||
Please select Company and Designation,Bitte wählen Sie Unternehmen und Position,
|
Please select Company and Designation,Bitte wählen Sie Unternehmen und Position,
|
||||||
Please select Company and Posting Date to getting entries,"Bitte wählen Sie Unternehmen und Buchungsdatum, um Einträge zu erhalten",
|
Please select Company and Posting Date to getting entries,"Bitte wählen Sie Unternehmen und Buchungsdatum, um Einträge zu erhalten",
|
||||||
Please select Company first,Bitte zuerst Unternehmen auswählen,
|
Please select Company first,Bitte zuerst Unternehmen auswählen,
|
||||||
Please select Completion Date for Completed Asset Maintenance Log,Bitte wählen Sie Fertigstellungsdatum für das abgeschlossene Wartungsprotokoll für den Vermögenswert,
|
Please select Completion Date for Completed Asset Maintenance Log,Bitte wählen Sie Fertigstellungsdatum für das abgeschlossene Wartungsprotokoll für den Vermögenswert,
|
||||||
Please select Completion Date for Completed Repair,Bitte wählen Sie das Abschlussdatum für die abgeschlossene Reparatur,
|
Please select Completion Date for Completed Repair,Bitte wählen Sie das Abschlussdatum für die abgeschlossene Reparatur,
|
||||||
Please select Course,Bitte wählen Sie Kurs,
|
|
||||||
Please select Drug,Bitte wählen Sie Arzneimittel,
|
|
||||||
Please select Employee,Bitte wählen Sie Mitarbeiter,
|
Please select Employee,Bitte wählen Sie Mitarbeiter,
|
||||||
Please select Existing Company for creating Chart of Accounts,Bitte wählen Sie Bestehende Unternehmen für die Erstellung von Konten,
|
Please select Existing Company for creating Chart of Accounts,Bitte wählen Sie Bestehende Unternehmen für die Erstellung von Konten,
|
||||||
Please select Healthcare Service,Bitte wählen Sie Gesundheitsdienst,
|
Please select Healthcare Service,Bitte wählen Sie Gesundheitsdienst,
|
||||||
@@ -7805,7 +7802,7 @@ Default Employee Advance Account,Standardkonto für Vorschüsse an Arbeitnehmer,
|
|||||||
Default Cost of Goods Sold Account,Standard-Herstellkosten,
|
Default Cost of Goods Sold Account,Standard-Herstellkosten,
|
||||||
Default Income Account,Standard-Ertragskonto,
|
Default Income Account,Standard-Ertragskonto,
|
||||||
Default Deferred Revenue Account,Standardkonto für passive Rechnungsabgrenzung,
|
Default Deferred Revenue Account,Standardkonto für passive Rechnungsabgrenzung,
|
||||||
Default Deferred Expense Account,Standard-Rechnungsabgrenzungsposten,
|
Default Deferred Expense Account,Standardkonto für aktive Rechnungsabgrenzung,
|
||||||
Default Payroll Payable Account,Standardkonto für Verbindlichkeiten aus Lohn und Gehalt,
|
Default Payroll Payable Account,Standardkonto für Verbindlichkeiten aus Lohn und Gehalt,
|
||||||
Default Expense Claim Payable Account,Standard-Expense Claim Zahlbares Konto,
|
Default Expense Claim Payable Account,Standard-Expense Claim Zahlbares Konto,
|
||||||
Stock Settings,Lager-Einstellungen,
|
Stock Settings,Lager-Einstellungen,
|
||||||
@@ -8873,7 +8870,7 @@ Add Topic to Courses,Hinzufügen eines Themas zu Kursen,
|
|||||||
This topic is already added to the existing courses,Dieses Thema wurde bereits zu den bestehenden Kursen hinzugefügt,
|
This topic is already added to the existing courses,Dieses Thema wurde bereits zu den bestehenden Kursen hinzugefügt,
|
||||||
"If Shopify does not have a customer in the order, then while syncing the orders, the system will consider the default customer for the order","Wenn Shopify keinen Kunden in der Bestellung hat, berücksichtigt das System beim Synchronisieren der Bestellungen den Standardkunden für die Bestellung",
|
"If Shopify does not have a customer in the order, then while syncing the orders, the system will consider the default customer for the order","Wenn Shopify keinen Kunden in der Bestellung hat, berücksichtigt das System beim Synchronisieren der Bestellungen den Standardkunden für die Bestellung",
|
||||||
The accounts are set by the system automatically but do confirm these defaults,"Die Konten werden vom System automatisch festgelegt, bestätigen jedoch diese Standardeinstellungen",
|
The accounts are set by the system automatically but do confirm these defaults,"Die Konten werden vom System automatisch festgelegt, bestätigen jedoch diese Standardeinstellungen",
|
||||||
Default Round Off Account,Standard-Rundungskonto,
|
Default Round Off Account,Standardkonto für Rundungsdifferenzen,
|
||||||
Failed Import Log,Importprotokoll fehlgeschlagen,
|
Failed Import Log,Importprotokoll fehlgeschlagen,
|
||||||
Fixed Error Log,Fehlerprotokoll behoben,
|
Fixed Error Log,Fehlerprotokoll behoben,
|
||||||
Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts,Firma {0} existiert bereits. Durch Fortfahren werden das Unternehmen und der Kontenplan überschrieben,
|
Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts,Firma {0} existiert bereits. Durch Fortfahren werden das Unternehmen und der Kontenplan überschrieben,
|
||||||
|
|||||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user