mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-02 03:39:11 +00:00
Merge pull request #46940 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -62,7 +62,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
|
|||||||
|
|
||||||
## Learning and community
|
## Learning and community
|
||||||
|
|
||||||
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
||||||
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
|
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
|
||||||
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
|
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
|
||||||
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
|
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
from erpnext import get_default_cost_center
|
from erpnext import get_default_cost_center
|
||||||
@@ -373,8 +374,6 @@ def auto_reconcile_vouchers(
|
|||||||
from_reference_date=None,
|
from_reference_date=None,
|
||||||
to_reference_date=None,
|
to_reference_date=None,
|
||||||
):
|
):
|
||||||
frappe.flags.auto_reconcile_vouchers = True
|
|
||||||
|
|
||||||
bank_transactions = get_bank_transactions(bank_account)
|
bank_transactions = get_bank_transactions(bank_account)
|
||||||
|
|
||||||
if len(bank_transactions) > 10:
|
if len(bank_transactions) > 10:
|
||||||
@@ -403,6 +402,8 @@ def auto_reconcile_vouchers(
|
|||||||
def start_auto_reconcile(
|
def start_auto_reconcile(
|
||||||
bank_transactions, from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date
|
bank_transactions, from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date
|
||||||
):
|
):
|
||||||
|
frappe.flags.auto_reconcile_vouchers = True
|
||||||
|
|
||||||
reconciled, partially_reconciled = set(), set()
|
reconciled, partially_reconciled = set(), set()
|
||||||
for transaction in bank_transactions:
|
for transaction in bank_transactions:
|
||||||
linked_payments = get_linked_payments(
|
linked_payments = get_linked_payments(
|
||||||
@@ -517,16 +518,23 @@ def subtract_allocations(gl_account, vouchers):
|
|||||||
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
|
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
|
||||||
|
|
||||||
for voucher in vouchers:
|
for voucher in vouchers:
|
||||||
rows = voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name"))) or []
|
if amount := get_allocated_amount(voucher_allocated_amounts, voucher, gl_account):
|
||||||
filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
|
|
||||||
|
|
||||||
if amount := None if not filtered_row else filtered_row[0]["total"]:
|
|
||||||
voucher["paid_amount"] -= amount
|
voucher["paid_amount"] -= amount
|
||||||
|
|
||||||
copied.append(voucher)
|
copied.append(voucher)
|
||||||
return copied
|
return copied
|
||||||
|
|
||||||
|
|
||||||
|
def get_allocated_amount(voucher_allocated_amounts, voucher, gl_account):
|
||||||
|
if not (voucher_details := voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name")))):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not (row := voucher_details.get(gl_account)):
|
||||||
|
return
|
||||||
|
|
||||||
|
return row.get("total")
|
||||||
|
|
||||||
|
|
||||||
def check_matching(
|
def check_matching(
|
||||||
bank_account,
|
bank_account,
|
||||||
company,
|
company,
|
||||||
@@ -796,26 +804,20 @@ def get_je_matching_query(
|
|||||||
je = frappe.qb.DocType("Journal Entry")
|
je = frappe.qb.DocType("Journal Entry")
|
||||||
jea = frappe.qb.DocType("Journal Entry Account")
|
jea = frappe.qb.DocType("Journal Entry Account")
|
||||||
|
|
||||||
ref_condition = je.cheque_no == transaction.reference_number
|
|
||||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
|
||||||
|
|
||||||
amount_field = f"{cr_or_dr}_in_account_currency"
|
amount_field = f"{cr_or_dr}_in_account_currency"
|
||||||
amount_equality = getattr(jea, amount_field) == transaction.unallocated_amount
|
|
||||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
|
||||||
|
|
||||||
filter_by_date = je.posting_date.between(from_date, to_date)
|
filter_by_date = je.posting_date.between(from_date, to_date)
|
||||||
if cint(filter_by_reference_date):
|
if cint(filter_by_reference_date):
|
||||||
filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date)
|
filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date)
|
||||||
|
|
||||||
query = (
|
subquery = (
|
||||||
frappe.qb.from_(jea)
|
frappe.qb.from_(jea)
|
||||||
.join(je)
|
.join(je)
|
||||||
.on(jea.parent == je.name)
|
.on(jea.parent == je.name)
|
||||||
.select(
|
.select(
|
||||||
(ref_rank + amount_rank + 1).as_("rank"),
|
Sum(getattr(jea, amount_field)).as_("paid_amount"),
|
||||||
ConstantColumn("Journal Entry").as_("doctype"),
|
ConstantColumn("Journal Entry").as_("doctype"),
|
||||||
je.name,
|
je.name,
|
||||||
getattr(jea, amount_field).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"),
|
||||||
@@ -827,13 +829,26 @@ def get_je_matching_query(
|
|||||||
.where(je.voucher_type != "Opening Entry")
|
.where(je.voucher_type != "Opening Entry")
|
||||||
.where(je.clearance_date.isnull())
|
.where(je.clearance_date.isnull())
|
||||||
.where(jea.account == common_filters.bank_account)
|
.where(jea.account == common_filters.bank_account)
|
||||||
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
|
|
||||||
.where(filter_by_date)
|
.where(filter_by_date)
|
||||||
|
.groupby(je.name)
|
||||||
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||||
)
|
)
|
||||||
|
|
||||||
if frappe.flags.auto_reconcile_vouchers is True:
|
if frappe.flags.auto_reconcile_vouchers is True:
|
||||||
query = query.where(ref_condition)
|
subquery = subquery.where(je.cheque_no == transaction.reference_number)
|
||||||
|
|
||||||
|
ref_rank = frappe.qb.terms.Case().when(subquery.reference_no == transaction.reference_number, 1).else_(0)
|
||||||
|
amount_equality = subquery.paid_amount == transaction.unallocated_amount
|
||||||
|
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(subquery)
|
||||||
|
.select(
|
||||||
|
"*",
|
||||||
|
(ref_rank + amount_rank + 1).as_("rank"),
|
||||||
|
)
|
||||||
|
.where(amount_equality if exact_match else subquery.paid_amount > 0.0)
|
||||||
|
)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,6 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("Bank Transaction", {
|
frappe.ui.form.on("Bank Transaction", {
|
||||||
onload(frm) {
|
|
||||||
frm.set_query("payment_document", "payment_entries", function () {
|
|
||||||
const payment_doctypes = frm.events.get_payment_doctypes(frm);
|
|
||||||
return {
|
|
||||||
filters: {
|
|
||||||
name: ["in", payment_doctypes],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
refresh(frm) {
|
|
||||||
if (!frm.is_dirty() && frm.doc.payment_entries.length > 0) {
|
|
||||||
frm.add_custom_button(__("Unreconcile Transaction"), () => {
|
|
||||||
frm.call("remove_payment_entries").then(() => frm.refresh());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bank_account: function (frm) {
|
|
||||||
set_bank_statement_filter(frm);
|
|
||||||
},
|
|
||||||
|
|
||||||
setup: function (frm) {
|
setup: function (frm) {
|
||||||
frm.set_query("party_type", function () {
|
frm.set_query("party_type", function () {
|
||||||
return {
|
return {
|
||||||
@@ -31,6 +10,41 @@ frappe.ui.form.on("Bank Transaction", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("bank_account", function () {
|
||||||
|
return {
|
||||||
|
filters: { is_company_account: 1 },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query("payment_document", "payment_entries", function () {
|
||||||
|
const payment_doctypes = frm.events.get_payment_doctypes(frm);
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
name: ["in", payment_doctypes],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query("payment_entry", "payment_entries", function () {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
docstatus: ["!=", 2],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh(frm) {
|
||||||
|
if (!frm.is_dirty() && frm.doc.payment_entries.length > 0) {
|
||||||
|
frm.add_custom_button(__("Unreconcile Transaction"), () => {
|
||||||
|
frm.call("remove_payment_entries").then(() => frm.refresh());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
bank_account: function (frm) {
|
||||||
|
set_bank_statement_filter(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
get_payment_doctypes: function () {
|
get_payment_doctypes: function () {
|
||||||
@@ -39,31 +53,6 @@ frappe.ui.form.on("Bank Transaction", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Bank Transaction Payments", {
|
|
||||||
payment_entries_remove: function (frm, cdt, cdn) {
|
|
||||||
update_clearance_date(frm, cdt, cdn);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const update_clearance_date = (frm, cdt, cdn) => {
|
|
||||||
if (frm.doc.docstatus === 1) {
|
|
||||||
frappe
|
|
||||||
.xcall("erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment", {
|
|
||||||
doctype: cdt,
|
|
||||||
docname: cdn,
|
|
||||||
bt_name: frm.doc.name,
|
|
||||||
})
|
|
||||||
.then((e) => {
|
|
||||||
if (e == "success") {
|
|
||||||
frappe.show_alert({
|
|
||||||
message: __("Document {0} successfully uncleared", [e]),
|
|
||||||
indicator: "green",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function set_bank_statement_filter(frm) {
|
function set_bank_statement_filter(frm) {
|
||||||
frm.set_query("bank_statement", function () {
|
frm.set_query("bank_statement", function () {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.docstatus import DocStatus
|
from frappe.model.docstatus import DocStatus
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt, getdate
|
||||||
|
|
||||||
|
|
||||||
class BankTransaction(Document):
|
class BankTransaction(Document):
|
||||||
@@ -84,16 +84,16 @@ class BankTransaction(Document):
|
|||||||
if not self.payment_entries:
|
if not self.payment_entries:
|
||||||
return
|
return
|
||||||
|
|
||||||
pe = []
|
references = set()
|
||||||
for row in self.payment_entries:
|
for row in self.payment_entries:
|
||||||
reference = (row.payment_document, row.payment_entry)
|
reference = (row.payment_document, row.payment_entry)
|
||||||
if reference in pe:
|
if reference in references:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("{0} {1} is allocated twice in this Bank Transaction").format(
|
_("{0} {1} is allocated twice in this Bank Transaction").format(
|
||||||
row.payment_document, row.payment_entry
|
row.payment_document, row.payment_entry
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
pe.append(reference)
|
references.add(reference)
|
||||||
|
|
||||||
def update_allocated_amount(self):
|
def update_allocated_amount(self):
|
||||||
allocated_amount = (
|
allocated_amount = (
|
||||||
@@ -104,6 +104,19 @@ class BankTransaction(Document):
|
|||||||
self.allocated_amount = flt(allocated_amount, self.precision("allocated_amount"))
|
self.allocated_amount = flt(allocated_amount, self.precision("allocated_amount"))
|
||||||
self.unallocated_amount = flt(unallocated_amount, self.precision("unallocated_amount"))
|
self.unallocated_amount = flt(unallocated_amount, self.precision("unallocated_amount"))
|
||||||
|
|
||||||
|
def delink_old_payment_entries(self):
|
||||||
|
if self.flags.updating_linked_bank_transaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
old_doc = self.get_doc_before_save()
|
||||||
|
payment_entry_names = set(pe.name for pe in self.payment_entries)
|
||||||
|
|
||||||
|
for old_pe in old_doc.payment_entries:
|
||||||
|
if old_pe.name in payment_entry_names:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.delink_payment_entry(old_pe)
|
||||||
|
|
||||||
def before_submit(self):
|
def before_submit(self):
|
||||||
self.allocate_payment_entries()
|
self.allocate_payment_entries()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
@@ -113,13 +126,14 @@ class BankTransaction(Document):
|
|||||||
|
|
||||||
def before_update_after_submit(self):
|
def before_update_after_submit(self):
|
||||||
self.validate_duplicate_references()
|
self.validate_duplicate_references()
|
||||||
self.allocate_payment_entries()
|
|
||||||
self.update_allocated_amount()
|
self.update_allocated_amount()
|
||||||
|
self.delink_old_payment_entries()
|
||||||
|
self.allocate_payment_entries()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
for payment_entry in self.payment_entries:
|
for payment_entry in self.payment_entries:
|
||||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
self.delink_payment_entry(payment_entry)
|
||||||
|
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
@@ -152,43 +166,55 @@ class BankTransaction(Document):
|
|||||||
- 0 > a: Error: already over-allocated
|
- 0 > a: Error: already over-allocated
|
||||||
- clear means: set the latest transaction date as clearance date
|
- clear means: set the latest transaction date as clearance date
|
||||||
"""
|
"""
|
||||||
|
if self.flags.updating_linked_bank_transaction or not self.payment_entries:
|
||||||
|
return
|
||||||
|
|
||||||
remaining_amount = self.unallocated_amount
|
remaining_amount = self.unallocated_amount
|
||||||
to_remove = []
|
|
||||||
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
|
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
|
||||||
pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
|
pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
|
||||||
|
gl_entries = get_related_bank_gl_entries(payment_entry_docs)
|
||||||
|
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
|
||||||
|
|
||||||
for payment_entry in self.payment_entries:
|
for payment_entry in list(self.payment_entries):
|
||||||
if payment_entry.allocated_amount == 0.0:
|
if payment_entry.allocated_amount != 0:
|
||||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
continue
|
||||||
self,
|
|
||||||
payment_entry,
|
allocable_amount, should_clear, clearance_date = get_clearance_details(
|
||||||
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry))
|
self,
|
||||||
or [],
|
payment_entry,
|
||||||
|
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry)) or {},
|
||||||
|
gl_entries.get((payment_entry.payment_document, payment_entry.payment_entry)) or {},
|
||||||
|
gl_bank_account,
|
||||||
|
)
|
||||||
|
|
||||||
|
if allocable_amount < 0:
|
||||||
|
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(allocable_amount))
|
||||||
|
|
||||||
|
if remaining_amount <= 0:
|
||||||
|
self.remove(payment_entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if allocable_amount == 0:
|
||||||
|
if should_clear:
|
||||||
|
self.clear_linked_payment_entry(payment_entry, clearance_date=clearance_date)
|
||||||
|
self.remove(payment_entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
should_clear = should_clear and allocable_amount <= remaining_amount
|
||||||
|
payment_entry.allocated_amount = min(allocable_amount, remaining_amount)
|
||||||
|
remaining_amount = flt(
|
||||||
|
remaining_amount - payment_entry.allocated_amount,
|
||||||
|
self.precision("unallocated_amount"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if payment_entry.payment_document == "Bank Transaction":
|
||||||
|
self.update_linked_bank_transaction(
|
||||||
|
payment_entry.payment_entry, payment_entry.allocated_amount
|
||||||
)
|
)
|
||||||
|
elif should_clear:
|
||||||
|
self.clear_linked_payment_entry(payment_entry, clearance_date=clearance_date)
|
||||||
|
|
||||||
if 0.0 == unallocated_amount:
|
self.update_allocated_amount()
|
||||||
if should_clear:
|
|
||||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
|
||||||
to_remove.append(payment_entry)
|
|
||||||
|
|
||||||
elif remaining_amount <= 0.0:
|
|
||||||
to_remove.append(payment_entry)
|
|
||||||
|
|
||||||
elif 0.0 < unallocated_amount <= remaining_amount:
|
|
||||||
payment_entry.allocated_amount = unallocated_amount
|
|
||||||
remaining_amount -= unallocated_amount
|
|
||||||
if should_clear:
|
|
||||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
|
||||||
|
|
||||||
elif 0.0 < unallocated_amount:
|
|
||||||
payment_entry.allocated_amount = remaining_amount
|
|
||||||
remaining_amount = 0.0
|
|
||||||
|
|
||||||
elif 0.0 > unallocated_amount:
|
|
||||||
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
|
||||||
|
|
||||||
for payment_entry in to_remove:
|
|
||||||
self.remove(payment_entry)
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def remove_payment_entries(self):
|
def remove_payment_entries(self):
|
||||||
@@ -199,14 +225,64 @@ class BankTransaction(Document):
|
|||||||
|
|
||||||
def remove_payment_entry(self, payment_entry):
|
def remove_payment_entry(self, payment_entry):
|
||||||
"Clear payment entry and clearance"
|
"Clear payment entry and clearance"
|
||||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
self.delink_payment_entry(payment_entry)
|
||||||
self.remove(payment_entry)
|
self.remove(payment_entry)
|
||||||
|
|
||||||
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
|
def delink_payment_entry(self, payment_entry):
|
||||||
clearance_date = None if for_cancel else self.date
|
if payment_entry.payment_document == "Bank Transaction":
|
||||||
set_voucher_clearance(
|
self.update_linked_bank_transaction(payment_entry.payment_entry, allocated_amount=None)
|
||||||
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
|
else:
|
||||||
)
|
self.clear_linked_payment_entry(payment_entry, clearance_date=None)
|
||||||
|
|
||||||
|
def clear_linked_payment_entry(self, payment_entry, clearance_date=None):
|
||||||
|
doctype = payment_entry.payment_document
|
||||||
|
docname = payment_entry.payment_entry
|
||||||
|
|
||||||
|
# might be a bank transaction
|
||||||
|
if doctype not in get_doctypes_for_bank_reconciliation():
|
||||||
|
return
|
||||||
|
|
||||||
|
if doctype == "Sales Invoice":
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Sales Invoice Payment",
|
||||||
|
dict(parenttype=doctype, parent=docname),
|
||||||
|
"clearance_date",
|
||||||
|
clearance_date,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
|
||||||
|
|
||||||
|
def update_linked_bank_transaction(self, bank_transaction_name, allocated_amount=None):
|
||||||
|
"""For when a second bank transaction has fixed another, e.g. refund"""
|
||||||
|
|
||||||
|
bt = frappe.get_doc(self.doctype, bank_transaction_name)
|
||||||
|
if allocated_amount:
|
||||||
|
bt.append(
|
||||||
|
"payment_entries",
|
||||||
|
{
|
||||||
|
"payment_document": self.doctype,
|
||||||
|
"payment_entry": self.name,
|
||||||
|
"allocated_amount": allocated_amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
pe = next(
|
||||||
|
(
|
||||||
|
pe
|
||||||
|
for pe in bt.payment_entries
|
||||||
|
if pe.payment_document == self.doctype and pe.payment_entry == self.name
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if not pe:
|
||||||
|
return
|
||||||
|
|
||||||
|
bt.flags.updating_linked_bank_transaction = True
|
||||||
|
bt.remove(pe)
|
||||||
|
|
||||||
|
bt.save()
|
||||||
|
|
||||||
def auto_set_party(self):
|
def auto_set_party(self):
|
||||||
from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty
|
from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty
|
||||||
@@ -238,71 +314,107 @@ def get_doctypes_for_bank_reconciliation():
|
|||||||
return frappe.get_hooks("bank_reconciliation_doctypes")
|
return frappe.get_hooks("bank_reconciliation_doctypes")
|
||||||
|
|
||||||
|
|
||||||
def get_clearance_details(transaction, payment_entry, bt_allocations):
|
def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries, gl_bank_account):
|
||||||
"""
|
"""
|
||||||
There should only be one bank gle for a voucher.
|
There should only be one bank gl entry for a voucher, except for JE.
|
||||||
Could be none for a Bank Transaction.
|
For JE, there can be multiple bank gl entries for the same account.
|
||||||
But if a JE, could affect two banks.
|
In this case, the allocable_amount will be the sum of amounts of all gl entries of the account.
|
||||||
Should only clear the voucher if all bank gles are allocated.
|
There will be no gl entry for a Bank Transaction so return the unallocated amount.
|
||||||
|
Should only clear the voucher if all bank gl entries 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)
|
|
||||||
|
|
||||||
unallocated_amount = min(
|
transaction_date = getdate(transaction.date)
|
||||||
transaction.unallocated_amount,
|
|
||||||
get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
|
if payment_entry.payment_document == "Bank Transaction":
|
||||||
)
|
bt = frappe.db.get_value(
|
||||||
unmatched_gles = len(gles)
|
"Bank Transaction",
|
||||||
latest_transaction = transaction
|
payment_entry.payment_entry,
|
||||||
for gle in gles:
|
("unallocated_amount", "bank_account"),
|
||||||
if gle["gl_account"] == gl_bank_account:
|
as_dict=True,
|
||||||
if gle["amount"] <= 0.0:
|
)
|
||||||
frappe.throw(
|
|
||||||
_("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"])
|
if bt.bank_account != gl_bank_account:
|
||||||
|
frappe.throw(
|
||||||
|
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
|
||||||
|
bt.bank_account, payment_entry.payment_entry, gl_bank_account
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
unmatched_gles -= 1
|
return abs(bt.unallocated_amount), True, transaction_date
|
||||||
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
|
if gl_bank_account not in gl_entries:
|
||||||
|
frappe.throw(
|
||||||
|
_("{} {} is not affecting bank account {}").format(
|
||||||
|
payment_entry.payment_document, payment_entry.payment_entry, gl_bank_account
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
allocable_amount = gl_entries.pop(gl_bank_account) or 0
|
||||||
|
if allocable_amount <= 0.0:
|
||||||
|
frappe.throw(
|
||||||
|
_("Invalid amount in accounting entries of {} {} for Account {}: {}").format(
|
||||||
|
payment_entry.payment_document, payment_entry.payment_entry, gl_bank_account, allocable_amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
matching_bt_allocaion = bt_allocations.pop(gl_bank_account, {})
|
||||||
|
|
||||||
|
allocable_amount = flt(
|
||||||
|
allocable_amount - matching_bt_allocaion.get("total", 0), transaction.precision("unallocated_amount")
|
||||||
|
)
|
||||||
|
|
||||||
|
should_clear = all(
|
||||||
|
gl_entries[gle_account] == bt_allocations.get(gle_account, {}).get("total", 0)
|
||||||
|
for gle_account in gl_entries
|
||||||
|
)
|
||||||
|
|
||||||
|
bt_allocation_date = matching_bt_allocaion.get("latest_date", None)
|
||||||
|
clearance_date = transaction_date if not bt_allocation_date else max(transaction_date, bt_allocation_date)
|
||||||
|
|
||||||
|
return allocable_amount, should_clear, clearance_date
|
||||||
|
|
||||||
|
|
||||||
def get_related_bank_gl_entries(doctype, docname):
|
def get_related_bank_gl_entries(docs):
|
||||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||||
return frappe.db.sql(
|
if not docs:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
|
gle.voucher_type AS doctype,
|
||||||
gle.account AS gl_account
|
gle.voucher_no AS docname,
|
||||||
FROM
|
gle.account AS gl_account,
|
||||||
`tabGL Entry` gle
|
SUM(ABS(gle.credit_in_account_currency - gle.debit_in_account_currency)) AS amount
|
||||||
LEFT JOIN
|
FROM
|
||||||
`tabAccount` ac ON ac.name=gle.account
|
`tabGL Entry` gle
|
||||||
WHERE
|
LEFT JOIN
|
||||||
ac.account_type = 'Bank'
|
`tabAccount` ac ON ac.name = gle.account
|
||||||
AND gle.voucher_type = %(doctype)s
|
WHERE
|
||||||
AND gle.voucher_no = %(docname)s
|
ac.account_type = 'Bank'
|
||||||
AND is_cancelled = 0
|
AND (gle.voucher_type, gle.voucher_no) IN %(docs)s
|
||||||
""",
|
AND gle.is_cancelled = 0
|
||||||
dict(doctype=doctype, docname=docname),
|
GROUP BY
|
||||||
|
gle.voucher_type, gle.voucher_no, gle.account
|
||||||
|
""",
|
||||||
|
{"docs": docs},
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
entries = {}
|
||||||
|
for row in result:
|
||||||
|
key = (row["doctype"], row["docname"])
|
||||||
|
if key not in entries:
|
||||||
|
entries[key] = {}
|
||||||
|
entries[key][row["gl_account"]] = row["amount"]
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def get_total_allocated_amount(docs):
|
def get_total_allocated_amount(docs):
|
||||||
"""
|
"""
|
||||||
Gets the sum of allocations for a voucher on each bank GL account
|
Gets the sum of allocations for a voucher on each bank GL account
|
||||||
along with the latest bank transaction name & date
|
along with the latest bank transaction date
|
||||||
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||||
"""
|
"""
|
||||||
if not docs:
|
if not docs:
|
||||||
@@ -311,11 +423,10 @@ def get_total_allocated_amount(docs):
|
|||||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||||
result = frappe.db.sql(
|
result = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
SELECT total, latest_name, latest_date, gl_account, payment_document, payment_entry FROM (
|
SELECT total, latest_date, gl_account, payment_document, payment_entry FROM (
|
||||||
SELECT
|
SELECT
|
||||||
ROW_NUMBER() OVER w AS rownum,
|
ROW_NUMBER() OVER w AS rownum,
|
||||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
|
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
|
||||||
FIRST_VALUE(bt.name) OVER w AS latest_name,
|
|
||||||
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||||
ba.account AS gl_account,
|
ba.account AS gl_account,
|
||||||
btp.payment_document,
|
btp.payment_document,
|
||||||
@@ -338,104 +449,14 @@ def get_total_allocated_amount(docs):
|
|||||||
|
|
||||||
payment_allocation_details = {}
|
payment_allocation_details = {}
|
||||||
for row in result:
|
for row in result:
|
||||||
# Why is this *sometimes* a byte string?
|
row["latest_date"] = getdate(row["latest_date"])
|
||||||
if isinstance(row["latest_name"], bytes):
|
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), {})[
|
||||||
row["latest_name"] = row["latest_name"].decode()
|
row["gl_account"]
|
||||||
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
|
] = row
|
||||||
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
|
|
||||||
|
|
||||||
return payment_allocation_details
|
return payment_allocation_details
|
||||||
|
|
||||||
|
|
||||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
|
||||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
|
||||||
paid_amount_field = "paid_amount"
|
|
||||||
if payment_entry.payment_document == "Payment Entry":
|
|
||||||
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
|
|
||||||
|
|
||||||
if doc.payment_type == "Receive":
|
|
||||||
paid_amount_field = (
|
|
||||||
"received_amount" if doc.paid_to_account_currency == currency else "base_received_amount"
|
|
||||||
)
|
|
||||||
elif doc.payment_type == "Pay":
|
|
||||||
paid_amount_field = (
|
|
||||||
"paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount"
|
|
||||||
)
|
|
||||||
|
|
||||||
return frappe.db.get_value(
|
|
||||||
payment_entry.payment_document, payment_entry.payment_entry, paid_amount_field
|
|
||||||
)
|
|
||||||
|
|
||||||
elif payment_entry.payment_document == "Journal Entry":
|
|
||||||
return abs(
|
|
||||||
frappe.db.get_value(
|
|
||||||
"Journal Entry Account",
|
|
||||||
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
|
|
||||||
"sum(debit_in_account_currency-credit_in_account_currency)",
|
|
||||||
)
|
|
||||||
or 0
|
|
||||||
)
|
|
||||||
|
|
||||||
elif payment_entry.payment_document == "Expense Claim":
|
|
||||||
return frappe.db.get_value(
|
|
||||||
payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif payment_entry.payment_document == "Loan Disbursement":
|
|
||||||
return frappe.db.get_value(
|
|
||||||
payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif payment_entry.payment_document == "Loan Repayment":
|
|
||||||
return frappe.db.get_value(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:
|
|
||||||
frappe.throw(
|
|
||||||
f"Please reconcile {payment_entry.payment_document}: {payment_entry.payment_entry} manually"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def set_voucher_clearance(doctype, docname, clearance_date, self):
|
|
||||||
if doctype in get_doctypes_for_bank_reconciliation():
|
|
||||||
if (
|
|
||||||
doctype == "Payment Entry"
|
|
||||||
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
|
|
||||||
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
if doctype == "Sales Invoice":
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Sales Invoice Payment",
|
|
||||||
dict(parenttype=doctype, parent=docname),
|
|
||||||
"clearance_date",
|
|
||||||
clearance_date,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
frappe.db.set_value(doctype, 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)
|
|
||||||
bt.save()
|
|
||||||
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):
|
def get_reconciled_bank_transactions(doctype, docname):
|
||||||
return frappe.get_all(
|
return frappe.get_all(
|
||||||
"Bank Transaction Payments",
|
"Bank Transaction Payments",
|
||||||
@@ -444,13 +465,6 @@ def get_reconciled_bank_transactions(doctype, docname):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
|
|
||||||
def remove_from_bank_transaction(doctype, docname):
|
def remove_from_bank_transaction(doctype, docname):
|
||||||
"""Remove a (cancelled) voucher from all Bank Transactions."""
|
"""Remove a (cancelled) voucher from all Bank Transactions."""
|
||||||
for bt_name in get_reconciled_bank_transactions(doctype, docname):
|
for bt_name in get_reconciled_bank_transactions(doctype, docname):
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str |
|
|||||||
if not language:
|
if not language:
|
||||||
language = doc.get("language")
|
language = doc.get("language")
|
||||||
|
|
||||||
|
letter_text = None
|
||||||
if language:
|
if language:
|
||||||
letter_text = frappe.db.get_value(
|
letter_text = frappe.db.get_value(
|
||||||
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
|
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
|
||||||
|
|||||||
@@ -249,16 +249,18 @@ class PaymentEntry(AccountsController):
|
|||||||
reference_names.add(key)
|
reference_names.add(key)
|
||||||
|
|
||||||
def set_bank_account_data(self):
|
def set_bank_account_data(self):
|
||||||
if self.bank_account:
|
if not self.bank_account:
|
||||||
bank_data = get_bank_account_details(self.bank_account)
|
return
|
||||||
|
|
||||||
field = "paid_from" if self.payment_type == "Pay" else "paid_to"
|
bank_data = get_bank_account_details(self.bank_account)
|
||||||
|
|
||||||
self.bank = bank_data.bank
|
field = "paid_from" if self.payment_type == "Pay" else "paid_to"
|
||||||
self.bank_account_no = bank_data.bank_account_no
|
|
||||||
|
|
||||||
if not self.get(field):
|
self.bank = bank_data.bank
|
||||||
self.set(field, bank_data.account)
|
self.bank_account_no = bank_data.bank_account_no
|
||||||
|
|
||||||
|
if not self.get(field):
|
||||||
|
self.set(field, bank_data.account)
|
||||||
|
|
||||||
def validate_payment_type_with_outstanding(self):
|
def validate_payment_type_with_outstanding(self):
|
||||||
total_outstanding = sum(d.allocated_amount for d in self.get("references"))
|
total_outstanding = sum(d.allocated_amount for d in self.get("references"))
|
||||||
@@ -276,15 +278,16 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
if self.party_type in ("Customer", "Supplier"):
|
if self.party_type in ("Customer", "Supplier"):
|
||||||
self.validate_allocated_amount_with_latest_data()
|
self.validate_allocated_amount_with_latest_data()
|
||||||
else:
|
return
|
||||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
|
||||||
for d in self.get("references"):
|
|
||||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(d.outstanding_amount):
|
|
||||||
frappe.throw(fail_message.format(d.idx))
|
|
||||||
|
|
||||||
# Check for negative outstanding invoices as well
|
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
for d in self.get("references"):
|
||||||
frappe.throw(fail_message.format(d.idx))
|
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||||
|
frappe.throw(fail_message.format(d.idx))
|
||||||
|
|
||||||
|
# Check for negative outstanding invoices as well
|
||||||
|
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||||
|
frappe.throw(fail_message.format(d.idx))
|
||||||
|
|
||||||
def validate_allocated_amount_as_per_payment_request(self):
|
def validate_allocated_amount_as_per_payment_request(self):
|
||||||
"""
|
"""
|
||||||
@@ -322,91 +325,89 @@ class PaymentEntry(AccountsController):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def validate_allocated_amount_with_latest_data(self):
|
def validate_allocated_amount_with_latest_data(self):
|
||||||
if self.references:
|
if not self.references:
|
||||||
uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references])
|
return
|
||||||
vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
|
|
||||||
latest_references = get_outstanding_reference_documents(
|
|
||||||
{
|
|
||||||
"posting_date": self.posting_date,
|
|
||||||
"company": self.company,
|
|
||||||
"party_type": self.party_type,
|
|
||||||
"payment_type": self.payment_type,
|
|
||||||
"party": self.party,
|
|
||||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
|
||||||
"get_outstanding_invoices": True,
|
|
||||||
"get_orders_to_be_billed": True,
|
|
||||||
"vouchers": vouchers,
|
|
||||||
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
|
|
||||||
},
|
|
||||||
validate=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Group latest_references by (voucher_type, voucher_no)
|
uniq_vouchers = {(x.reference_doctype, x.reference_name) for x in self.references}
|
||||||
latest_lookup = {}
|
vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
|
||||||
for d in latest_references:
|
latest_references = get_outstanding_reference_documents(
|
||||||
d = frappe._dict(d)
|
{
|
||||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
"posting_date": self.posting_date,
|
||||||
|
"company": self.company,
|
||||||
|
"party_type": self.party_type,
|
||||||
|
"payment_type": self.payment_type,
|
||||||
|
"party": self.party,
|
||||||
|
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||||
|
"get_outstanding_invoices": True,
|
||||||
|
"get_orders_to_be_billed": True,
|
||||||
|
"vouchers": vouchers,
|
||||||
|
"book_advance_payments_in_separate_party_account": self.book_advance_payments_in_separate_party_account,
|
||||||
|
},
|
||||||
|
validate=True,
|
||||||
|
)
|
||||||
|
|
||||||
for idx, d in enumerate(self.get("references"), start=1):
|
# Group latest_references by (voucher_type, voucher_no)
|
||||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
latest_lookup = {}
|
||||||
|
for d in latest_references:
|
||||||
|
d = frappe._dict(d)
|
||||||
|
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||||
|
|
||||||
# If term based allocation is enabled, throw
|
for idx, d in enumerate(self.get("references"), start=1):
|
||||||
if (
|
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
||||||
d.payment_term is None or d.payment_term == ""
|
|
||||||
) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name):
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
|
||||||
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
|
||||||
)
|
|
||||||
|
|
||||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
# If term based allocation is enabled, throw
|
||||||
latest = latest.get(d.payment_term) or latest.get(None)
|
if (
|
||||||
# The reference has already been fully paid
|
d.payment_term is None or d.payment_term == ""
|
||||||
if not latest:
|
) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("{0} {1} has already been fully paid.").format(
|
_(
|
||||||
_(d.reference_doctype), d.reference_name
|
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||||
)
|
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
||||||
)
|
)
|
||||||
# The reference has already been partly paid
|
|
||||||
elif (
|
|
||||||
latest.outstanding_amount < latest.invoice_amount
|
|
||||||
and flt(d.outstanding_amount, d.precision("outstanding_amount"))
|
|
||||||
!= flt(latest.outstanding_amount, d.precision("outstanding_amount"))
|
|
||||||
and d.payment_term == ""
|
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
|
||||||
).format(_(d.reference_doctype), d.reference_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||||
|
latest = latest.get(d.payment_term) or latest.get(None)
|
||||||
|
# The reference has already been fully paid
|
||||||
|
if not latest:
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||||
|
)
|
||||||
|
# The reference has already been partly paid
|
||||||
|
elif (
|
||||||
|
latest.outstanding_amount < latest.invoice_amount
|
||||||
|
and flt(d.outstanding_amount, d.precision("outstanding_amount"))
|
||||||
|
!= flt(latest.outstanding_amount, d.precision("outstanding_amount"))
|
||||||
|
and d.payment_term == ""
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||||
|
).format(_(d.reference_doctype), d.reference_name)
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||||
d.payment_term
|
|
||||||
and (
|
|
||||||
(flt(d.allocated_amount)) > 0
|
|
||||||
and latest.payment_term_outstanding
|
|
||||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
|
||||||
)
|
|
||||||
and self.term_based_allocation_enabled_for_reference(
|
|
||||||
d.reference_doctype, d.reference_name
|
|
||||||
)
|
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
|
||||||
).format(d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
if (
|
||||||
frappe.throw(fail_message.format(d.idx))
|
d.payment_term
|
||||||
|
and (
|
||||||
|
(flt(d.allocated_amount)) > 0
|
||||||
|
and latest.payment_term_outstanding
|
||||||
|
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||||
|
)
|
||||||
|
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||||
|
).format(d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term)
|
||||||
|
)
|
||||||
|
|
||||||
# Check for negative outstanding invoices as well
|
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
frappe.throw(fail_message.format(d.idx))
|
||||||
frappe.throw(fail_message.format(d.idx))
|
|
||||||
|
# Check for negative outstanding invoices as well
|
||||||
|
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||||
|
frappe.throw(fail_message.format(d.idx))
|
||||||
|
|
||||||
def delink_advance_entry_references(self):
|
def delink_advance_entry_references(self):
|
||||||
for reference in self.references:
|
for reference in self.references:
|
||||||
@@ -479,47 +480,48 @@ class PaymentEntry(AccountsController):
|
|||||||
reference_exchange_details: dict | None = None,
|
reference_exchange_details: dict | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
for d in self.get("references"):
|
for d in self.get("references"):
|
||||||
if d.allocated_amount:
|
if not d.allocated_amount:
|
||||||
if update_ref_details_only_for and (
|
continue
|
||||||
(d.reference_doctype, d.reference_name) not in update_ref_details_only_for
|
|
||||||
):
|
if update_ref_details_only_for and (
|
||||||
|
(d.reference_doctype, d.reference_name) not in update_ref_details_only_for
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
ref_details = get_reference_details(
|
||||||
|
d.reference_doctype,
|
||||||
|
d.reference_name,
|
||||||
|
self.party_account_currency,
|
||||||
|
self.party_type,
|
||||||
|
self.party,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only update exchange rate when the reference is Journal Entry
|
||||||
|
if (
|
||||||
|
reference_exchange_details
|
||||||
|
and d.reference_doctype == reference_exchange_details.reference_doctype
|
||||||
|
and d.reference_name == reference_exchange_details.reference_name
|
||||||
|
):
|
||||||
|
ref_details.update({"exchange_rate": reference_exchange_details.exchange_rate})
|
||||||
|
|
||||||
|
for field, value in ref_details.items():
|
||||||
|
if d.exchange_gain_loss:
|
||||||
|
# for cases where gain/loss is booked into invoice
|
||||||
|
# exchange_gain_loss is calculated from invoice & populated
|
||||||
|
# and row.exchange_rate is already set to payment entry's exchange rate
|
||||||
|
# refer -> `update_reference_in_payment_entry()` in utils.py
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ref_details = get_reference_details(
|
if field == "exchange_rate" or not d.get(field) or force:
|
||||||
d.reference_doctype,
|
d.db_set(field, value)
|
||||||
d.reference_name,
|
|
||||||
self.party_account_currency,
|
|
||||||
self.party_type,
|
|
||||||
self.party,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only update exchange rate when the reference is Journal Entry
|
|
||||||
if (
|
|
||||||
reference_exchange_details
|
|
||||||
and d.reference_doctype == reference_exchange_details.reference_doctype
|
|
||||||
and d.reference_name == reference_exchange_details.reference_name
|
|
||||||
):
|
|
||||||
ref_details.update({"exchange_rate": reference_exchange_details.exchange_rate})
|
|
||||||
|
|
||||||
for field, value in ref_details.items():
|
|
||||||
if d.exchange_gain_loss:
|
|
||||||
# for cases where gain/loss is booked into invoice
|
|
||||||
# exchange_gain_loss is calculated from invoice & populated
|
|
||||||
# and row.exchange_rate is already set to payment entry's exchange rate
|
|
||||||
# refer -> `update_reference_in_payment_entry()` in utils.py
|
|
||||||
continue
|
|
||||||
|
|
||||||
if field == "exchange_rate" or not d.get(field) or force:
|
|
||||||
d.db_set(field, value)
|
|
||||||
|
|
||||||
def validate_payment_type(self):
|
def validate_payment_type(self):
|
||||||
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
|
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
|
||||||
frappe.throw(_("Payment Type must be one of Receive, Pay and Internal Transfer"))
|
frappe.throw(_("Payment Type must be one of Receive, Pay and Internal Transfer"))
|
||||||
|
|
||||||
def validate_party_details(self):
|
def validate_party_details(self):
|
||||||
if self.party:
|
if self.party and not frappe.db.exists(self.party_type, self.party):
|
||||||
if not frappe.db.exists(self.party_type, self.party):
|
frappe.throw(_("{0} {1} does not exist").format(_(self.party_type), self.party))
|
||||||
frappe.throw(_("{0} {1} does not exist").format(_(self.party_type), self.party))
|
|
||||||
|
|
||||||
def set_exchange_rate(self, ref_doc=None):
|
def set_exchange_rate(self, ref_doc=None):
|
||||||
self.set_source_exchange_rate(ref_doc)
|
self.set_source_exchange_rate(ref_doc)
|
||||||
@@ -529,12 +531,8 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.paid_from:
|
if self.paid_from:
|
||||||
if self.paid_from_account_currency == self.company_currency:
|
if self.paid_from_account_currency == self.company_currency:
|
||||||
self.source_exchange_rate = 1
|
self.source_exchange_rate = 1
|
||||||
else:
|
elif ref_doc and self.paid_from_account_currency == ref_doc.currency:
|
||||||
if ref_doc:
|
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||||
if self.paid_from_account_currency == ref_doc.currency:
|
|
||||||
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get(
|
|
||||||
"conversion_rate"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.source_exchange_rate:
|
if not self.source_exchange_rate:
|
||||||
self.source_exchange_rate = get_exchange_rate(
|
self.source_exchange_rate = get_exchange_rate(
|
||||||
@@ -545,9 +543,8 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.paid_from_account_currency == self.paid_to_account_currency:
|
if self.paid_from_account_currency == self.paid_to_account_currency:
|
||||||
self.target_exchange_rate = self.source_exchange_rate
|
self.target_exchange_rate = self.source_exchange_rate
|
||||||
elif self.paid_to and not self.target_exchange_rate:
|
elif self.paid_to and not self.target_exchange_rate:
|
||||||
if ref_doc:
|
if ref_doc and self.paid_to_account_currency == ref_doc.currency:
|
||||||
if self.paid_to_account_currency == ref_doc.currency:
|
self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||||
self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
|
||||||
|
|
||||||
if not self.target_exchange_rate:
|
if not self.target_exchange_rate:
|
||||||
self.target_exchange_rate = get_exchange_rate(
|
self.target_exchange_rate = get_exchange_rate(
|
||||||
@@ -578,63 +575,61 @@ class PaymentEntry(AccountsController):
|
|||||||
elif d.reference_name:
|
elif d.reference_name:
|
||||||
if not frappe.db.exists(d.reference_doctype, d.reference_name):
|
if not frappe.db.exists(d.reference_doctype, d.reference_name):
|
||||||
frappe.throw(_("{0} {1} does not exist").format(d.reference_doctype, d.reference_name))
|
frappe.throw(_("{0} {1} does not exist").format(d.reference_doctype, d.reference_name))
|
||||||
else:
|
|
||||||
ref_doc = frappe.get_doc(d.reference_doctype, d.reference_name)
|
|
||||||
|
|
||||||
if d.reference_doctype != "Journal Entry":
|
ref_doc = frappe.get_doc(d.reference_doctype, d.reference_name)
|
||||||
if self.party != ref_doc.get(scrub(self.party_type)):
|
|
||||||
frappe.throw(
|
|
||||||
_("{0} {1} is not associated with {2} {3}").format(
|
|
||||||
_(d.reference_doctype), d.reference_name, _(self.party_type), self.party
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.validate_journal_entry()
|
|
||||||
|
|
||||||
if d.reference_doctype in frappe.get_hooks("invoice_doctypes"):
|
if d.reference_doctype != "Journal Entry":
|
||||||
if self.party_type == "Customer":
|
if self.party != ref_doc.get(scrub(self.party_type)):
|
||||||
ref_party_account = (
|
|
||||||
get_party_account_based_on_invoice_discounting(d.reference_name)
|
|
||||||
or ref_doc.debit_to
|
|
||||||
)
|
|
||||||
elif self.party_type == "Supplier":
|
|
||||||
ref_party_account = ref_doc.credit_to
|
|
||||||
elif self.party_type == "Employee":
|
|
||||||
ref_party_account = ref_doc.payable_account
|
|
||||||
|
|
||||||
if (
|
|
||||||
ref_party_account != self.party_account
|
|
||||||
and not self.book_advance_payments_in_separate_party_account
|
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_("{0} {1} is associated with {2}, but Party Account is {3}").format(
|
|
||||||
_(d.reference_doctype),
|
|
||||||
d.reference_name,
|
|
||||||
ref_party_account,
|
|
||||||
self.party_account,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
|
|
||||||
frappe.throw(
|
|
||||||
_("{0} {1} is on hold").format(_(d.reference_doctype), d.reference_name),
|
|
||||||
title=_("Invalid Purchase Invoice"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if ref_doc.docstatus != 1:
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name)
|
_("{0} {1} is not associated with {2} {3}").format(
|
||||||
|
_(d.reference_doctype), d.reference_name, _(self.party_type), self.party
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self.validate_journal_entry()
|
||||||
|
|
||||||
|
if d.reference_doctype in frappe.get_hooks("invoice_doctypes"):
|
||||||
|
if self.party_type == "Customer":
|
||||||
|
ref_party_account = (
|
||||||
|
get_party_account_based_on_invoice_discounting(d.reference_name)
|
||||||
|
or ref_doc.debit_to
|
||||||
|
)
|
||||||
|
elif self.party_type == "Supplier":
|
||||||
|
ref_party_account = ref_doc.credit_to
|
||||||
|
elif self.party_type == "Employee":
|
||||||
|
ref_party_account = ref_doc.payable_account
|
||||||
|
|
||||||
|
if (
|
||||||
|
ref_party_account != self.party_account
|
||||||
|
and not self.book_advance_payments_in_separate_party_account
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} {1} is associated with {2}, but Party Account is {3}").format(
|
||||||
|
_(d.reference_doctype),
|
||||||
|
d.reference_name,
|
||||||
|
ref_party_account,
|
||||||
|
self.party_account,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} {1} is on hold").format(_(d.reference_doctype), d.reference_name),
|
||||||
|
title=_("Invalid Purchase Invoice"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ref_doc.docstatus != 1:
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name)
|
||||||
|
)
|
||||||
|
|
||||||
def get_valid_reference_doctypes(self):
|
def get_valid_reference_doctypes(self):
|
||||||
if self.party_type == "Customer":
|
if self.party_type == "Customer":
|
||||||
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning", "Payment Entry")
|
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning", "Payment Entry")
|
||||||
|
elif self.party_type in ["Shareholder", "Employee"]:
|
||||||
|
return ("Journal Entry",)
|
||||||
elif self.party_type == "Supplier":
|
elif self.party_type == "Supplier":
|
||||||
return ("Purchase Order", "Purchase Invoice", "Journal Entry", "Payment Entry")
|
return ("Purchase Order", "Purchase Invoice", "Journal Entry", "Payment Entry")
|
||||||
elif self.party_type == "Shareholder":
|
|
||||||
return ("Journal Entry",)
|
|
||||||
elif self.party_type == "Employee":
|
|
||||||
return ("Journal Entry",)
|
|
||||||
|
|
||||||
def validate_paid_invoices(self):
|
def validate_paid_invoices(self):
|
||||||
no_oustanding_refs = {}
|
no_oustanding_refs = {}
|
||||||
@@ -700,37 +695,39 @@ class PaymentEntry(AccountsController):
|
|||||||
invoice_paid_amount_map = {}
|
invoice_paid_amount_map = {}
|
||||||
|
|
||||||
for ref in self.get("references"):
|
for ref in self.get("references"):
|
||||||
if ref.payment_term and ref.reference_name:
|
if not ref.payment_term or not ref.reference_name:
|
||||||
key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
|
continue
|
||||||
invoice_payment_amount_map.setdefault(key, 0.0)
|
|
||||||
invoice_payment_amount_map[key] += ref.allocated_amount
|
|
||||||
|
|
||||||
if not invoice_paid_amount_map.get(key):
|
key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
|
||||||
payment_schedule = frappe.get_all(
|
invoice_payment_amount_map.setdefault(key, 0.0)
|
||||||
"Payment Schedule",
|
invoice_payment_amount_map[key] += ref.allocated_amount
|
||||||
filters={"parent": ref.reference_name},
|
|
||||||
fields=[
|
|
||||||
"paid_amount",
|
|
||||||
"payment_amount",
|
|
||||||
"payment_term",
|
|
||||||
"discount",
|
|
||||||
"outstanding",
|
|
||||||
"discount_type",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
for term in payment_schedule:
|
|
||||||
invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
|
|
||||||
invoice_paid_amount_map.setdefault(invoice_key, {})
|
|
||||||
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
|
|
||||||
if not (term.discount_type and term.discount):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if term.discount_type == "Percentage":
|
if not invoice_paid_amount_map.get(key):
|
||||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
payment_schedule = frappe.get_all(
|
||||||
term.discount / 100
|
"Payment Schedule",
|
||||||
)
|
filters={"parent": ref.reference_name},
|
||||||
else:
|
fields=[
|
||||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
|
"paid_amount",
|
||||||
|
"payment_amount",
|
||||||
|
"payment_term",
|
||||||
|
"discount",
|
||||||
|
"outstanding",
|
||||||
|
"discount_type",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
for term in payment_schedule:
|
||||||
|
invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
|
||||||
|
invoice_paid_amount_map.setdefault(invoice_key, {})
|
||||||
|
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
|
||||||
|
if not (term.discount_type and term.discount):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if term.discount_type == "Percentage":
|
||||||
|
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
||||||
|
term.discount / 100
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
|
||||||
|
|
||||||
for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
|
for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
|
||||||
if not invoice_paid_amount_map.get(key):
|
if not invoice_paid_amount_map.get(key):
|
||||||
@@ -977,14 +974,14 @@ class PaymentEntry(AccountsController):
|
|||||||
applicable_tax = 0
|
applicable_tax = 0
|
||||||
base_applicable_tax = 0
|
base_applicable_tax = 0
|
||||||
for tax in self.get("taxes"):
|
for tax in self.get("taxes"):
|
||||||
if not tax.included_in_paid_amount:
|
if tax.included_in_paid_amount:
|
||||||
amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount
|
continue
|
||||||
base_amount = (
|
|
||||||
-1 * tax.base_tax_amount if tax.add_deduct_tax == "Deduct" else tax.base_tax_amount
|
|
||||||
)
|
|
||||||
|
|
||||||
applicable_tax += amount
|
amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount
|
||||||
base_applicable_tax += base_amount
|
base_amount = -1 * tax.base_tax_amount if tax.add_deduct_tax == "Deduct" else tax.base_tax_amount
|
||||||
|
|
||||||
|
applicable_tax += amount
|
||||||
|
base_applicable_tax += base_amount
|
||||||
|
|
||||||
self.paid_amount_after_tax = flt(
|
self.paid_amount_after_tax = flt(
|
||||||
flt(self.paid_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax")
|
flt(self.paid_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax")
|
||||||
@@ -1648,25 +1645,27 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
def add_deductions_gl_entries(self, gl_entries):
|
def add_deductions_gl_entries(self, gl_entries):
|
||||||
for d in self.get("deductions"):
|
for d in self.get("deductions"):
|
||||||
if d.amount:
|
if not d.amount:
|
||||||
account_currency = get_account_currency(d.account)
|
continue
|
||||||
if account_currency != self.company_currency:
|
|
||||||
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
|
|
||||||
|
|
||||||
gl_entries.append(
|
account_currency = get_account_currency(d.account)
|
||||||
self.get_gl_dict(
|
if account_currency != self.company_currency:
|
||||||
{
|
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
|
||||||
"account": d.account,
|
|
||||||
"account_currency": account_currency,
|
gl_entries.append(
|
||||||
"against": self.party or self.paid_from,
|
self.get_gl_dict(
|
||||||
"debit_in_account_currency": d.amount,
|
{
|
||||||
"debit_in_transaction_currency": d.amount / self.transaction_exchange_rate,
|
"account": d.account,
|
||||||
"debit": d.amount,
|
"account_currency": account_currency,
|
||||||
"cost_center": d.cost_center,
|
"against": self.party or self.paid_from,
|
||||||
},
|
"debit_in_account_currency": d.amount,
|
||||||
item=d,
|
"debit_in_transaction_currency": d.amount / self.transaction_exchange_rate,
|
||||||
)
|
"debit": d.amount,
|
||||||
|
"cost_center": d.cost_center,
|
||||||
|
},
|
||||||
|
item=d,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_party_account_for_taxes(self):
|
def get_party_account_for_taxes(self):
|
||||||
if self.payment_type == "Receive":
|
if self.payment_type == "Receive":
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.functions import Abs, Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
from frappe.utils.background_jobs import enqueue
|
from frappe.utils.background_jobs import enqueue
|
||||||
|
|
||||||
@@ -12,7 +12,6 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
|||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||||
get_company_defaults,
|
|
||||||
get_payment_entry,
|
get_payment_entry,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||||
@@ -120,13 +119,13 @@ class PaymentRequest(Document):
|
|||||||
title=_("Invalid Amount"),
|
title=_("Invalid Amount"),
|
||||||
)
|
)
|
||||||
|
|
||||||
existing_payment_request_amount = flt(
|
|
||||||
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||||
if not hasattr(ref_doc, "order_type") or ref_doc.order_type != "Shopping Cart":
|
if not hasattr(ref_doc, "order_type") or ref_doc.order_type != "Shopping Cart":
|
||||||
ref_amount = get_amount(ref_doc, self.payment_account)
|
ref_amount = get_amount(ref_doc, self.payment_account)
|
||||||
|
if not ref_amount:
|
||||||
|
frappe.throw(_("Payment Entry is already created"))
|
||||||
|
|
||||||
|
existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))
|
||||||
|
|
||||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
@@ -544,6 +543,8 @@ def make_payment_request(**args):
|
|||||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||||
|
|
||||||
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
|
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
|
||||||
|
if not grand_total:
|
||||||
|
frappe.throw(_("Payment Entry is already created"))
|
||||||
if args.loyalty_points and args.dt == "Sales Order":
|
if args.loyalty_points and args.dt == "Sales Order":
|
||||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
||||||
|
|
||||||
@@ -554,19 +555,8 @@ def make_payment_request(**args):
|
|||||||
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
|
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
|
||||||
grand_total = grand_total - loyalty_amount
|
grand_total = grand_total - loyalty_amount
|
||||||
|
|
||||||
bank_account = (
|
|
||||||
get_party_bank_account(args.get("party_type"), args.get("party")) if args.get("party_type") else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
draft_payment_request = frappe.db.get_value(
|
|
||||||
"Payment Request",
|
|
||||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
|
||||||
)
|
|
||||||
|
|
||||||
# fetches existing payment request `grand_total` amount
|
# fetches existing payment request `grand_total` amount
|
||||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc)
|
||||||
|
|
||||||
existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name)
|
|
||||||
|
|
||||||
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
|
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
|
||||||
grand_total -= existing_payment_request_amount
|
grand_total -= existing_payment_request_amount
|
||||||
@@ -578,7 +568,7 @@ def make_payment_request(**args):
|
|||||||
if args.order_type == "Shopping Cart":
|
if args.order_type == "Shopping Cart":
|
||||||
# If Payment Request is in an advanced stage, then create for remaining amount.
|
# If Payment Request is in an advanced stage, then create for remaining amount.
|
||||||
if get_existing_payment_request_amount(
|
if get_existing_payment_request_amount(
|
||||||
ref_doc.doctype, ref_doc.name, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"]
|
ref_doc, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"]
|
||||||
):
|
):
|
||||||
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||||
else:
|
else:
|
||||||
@@ -587,14 +577,10 @@ def make_payment_request(**args):
|
|||||||
else:
|
else:
|
||||||
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||||
|
|
||||||
if existing_paid_amount:
|
draft_payment_request = frappe.db.get_value(
|
||||||
if ref_doc.party_account_currency == ref_doc.currency:
|
"Payment Request",
|
||||||
if ref_doc.conversion_rate:
|
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
|
||||||
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
)
|
||||||
else:
|
|
||||||
grand_total -= flt(existing_paid_amount)
|
|
||||||
else:
|
|
||||||
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
|
||||||
|
|
||||||
if draft_payment_request:
|
if draft_payment_request:
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
@@ -602,6 +588,11 @@ def make_payment_request(**args):
|
|||||||
)
|
)
|
||||||
pr = frappe.get_doc("Payment Request", draft_payment_request)
|
pr = frappe.get_doc("Payment Request", draft_payment_request)
|
||||||
else:
|
else:
|
||||||
|
bank_account = (
|
||||||
|
get_party_bank_account(args.get("party_type"), args.get("party"))
|
||||||
|
if args.get("party_type")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
pr = frappe.new_doc("Payment Request")
|
pr = frappe.new_doc("Payment Request")
|
||||||
|
|
||||||
if not args.get("payment_request_type"):
|
if not args.get("payment_request_type"):
|
||||||
@@ -675,22 +666,35 @@ def make_payment_request(**args):
|
|||||||
|
|
||||||
def get_amount(ref_doc, payment_account=None):
|
def get_amount(ref_doc, payment_account=None):
|
||||||
"""get amount based on doctype"""
|
"""get amount based on doctype"""
|
||||||
|
grand_total = 0
|
||||||
|
|
||||||
dt = ref_doc.doctype
|
dt = ref_doc.doctype
|
||||||
if dt in ["Sales Order", "Purchase Order"]:
|
if dt in ["Sales Order", "Purchase Order"]:
|
||||||
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
|
grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - ref_doc.advance_paid
|
||||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
if not ref_doc.get("is_pos"):
|
if (
|
||||||
|
dt == "Sales Invoice"
|
||||||
|
and ref_doc.is_pos
|
||||||
|
and ref_doc.payments
|
||||||
|
and any(
|
||||||
|
[
|
||||||
|
payment.type == "Phone" and payment.account == payment_account
|
||||||
|
for payment in ref_doc.payments
|
||||||
|
]
|
||||||
|
)
|
||||||
|
):
|
||||||
|
grand_total = sum(
|
||||||
|
[
|
||||||
|
payment.amount
|
||||||
|
for payment in ref_doc.payments
|
||||||
|
if payment.type == "Phone" and payment.account == payment_account
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
if ref_doc.party_account_currency == ref_doc.currency:
|
if ref_doc.party_account_currency == ref_doc.currency:
|
||||||
grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total)
|
grand_total = flt(ref_doc.outstanding_amount)
|
||||||
else:
|
else:
|
||||||
grand_total = flt(
|
grand_total = flt(flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate)
|
||||||
flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate
|
|
||||||
)
|
|
||||||
elif dt == "Sales Invoice":
|
|
||||||
for pay in ref_doc.payments:
|
|
||||||
if pay.type == "Phone" and pay.account == payment_account:
|
|
||||||
grand_total = pay.amount
|
|
||||||
break
|
|
||||||
elif dt == "POS Invoice":
|
elif dt == "POS Invoice":
|
||||||
for pay in ref_doc.payments:
|
for pay in ref_doc.payments:
|
||||||
if pay.type == "Phone" and pay.account == payment_account:
|
if pay.type == "Phone" and pay.account == payment_account:
|
||||||
@@ -699,10 +703,7 @@ def get_amount(ref_doc, payment_account=None):
|
|||||||
elif dt == "Fees":
|
elif dt == "Fees":
|
||||||
grand_total = ref_doc.outstanding_amount
|
grand_total = ref_doc.outstanding_amount
|
||||||
|
|
||||||
if grand_total > 0:
|
return flt(grand_total, get_currency_precision()) if grand_total > 0 else 0
|
||||||
return flt(grand_total, get_currency_precision())
|
|
||||||
else:
|
|
||||||
frappe.throw(_("Payment Entry is already created"))
|
|
||||||
|
|
||||||
|
|
||||||
def get_irequest_status(payment_requests: None | list = None) -> list:
|
def get_irequest_status(payment_requests: None | list = None) -> list:
|
||||||
@@ -745,7 +746,7 @@ def cancel_old_payment_requests(ref_dt, ref_dn):
|
|||||||
frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled")
|
frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled")
|
||||||
|
|
||||||
|
|
||||||
def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None = None) -> list:
|
def get_existing_payment_request_amount(ref_doc, statuses: list | None = None) -> list:
|
||||||
"""
|
"""
|
||||||
Return the total amount of Payment Requests against a reference document.
|
Return the total amount of Payment Requests against a reference document.
|
||||||
"""
|
"""
|
||||||
@@ -753,9 +754,9 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
|
|||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(PR)
|
frappe.qb.from_(PR)
|
||||||
.select(Sum(PR.grand_total))
|
.select(Sum(PR.outstanding_amount))
|
||||||
.where(PR.reference_doctype == ref_dt)
|
.where(PR.reference_doctype == ref_doc.doctype)
|
||||||
.where(PR.reference_name == ref_dn)
|
.where(PR.reference_name == ref_doc.name)
|
||||||
.where(PR.docstatus == 1)
|
.where(PR.docstatus == 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -764,43 +765,12 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
|
|||||||
|
|
||||||
response = query.run()
|
response = query.run()
|
||||||
|
|
||||||
return response[0][0] if response[0] else 0
|
os_amount_in_transaction_currency = flt(response[0][0] if response[0] else 0)
|
||||||
|
|
||||||
|
if ref_doc.currency != ref_doc.party_account_currency:
|
||||||
|
os_amount_in_transaction_currency = flt(os_amount_in_transaction_currency / ref_doc.conversion_rate)
|
||||||
|
|
||||||
def get_existing_paid_amount(doctype, name):
|
return os_amount_in_transaction_currency
|
||||||
PLE = frappe.qb.DocType("Payment Ledger Entry")
|
|
||||||
PER = frappe.qb.DocType("Payment Entry Reference")
|
|
||||||
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(PLE)
|
|
||||||
.left_join(PER)
|
|
||||||
.on(
|
|
||||||
(PLE.against_voucher_type == PER.reference_doctype)
|
|
||||||
& (PLE.against_voucher_no == PER.reference_name)
|
|
||||||
& (PLE.voucher_type == PER.parenttype)
|
|
||||||
& (PLE.voucher_no == PER.parent)
|
|
||||||
)
|
|
||||||
.select(
|
|
||||||
Abs(Sum(PLE.amount)).as_("total_amount"),
|
|
||||||
Abs(Sum(frappe.qb.terms.Case().when(PER.payment_request.isnotnull(), PLE.amount).else_(0))).as_(
|
|
||||||
"request_paid_amount"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
(PLE.voucher_type.isin([doctype, "Journal Entry", "Payment Entry"]))
|
|
||||||
& (PLE.against_voucher_type == doctype)
|
|
||||||
& (PLE.against_voucher_no == name)
|
|
||||||
& (PLE.delinked == 0)
|
|
||||||
& (PLE.docstatus == 1)
|
|
||||||
& (PLE.amount < 0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
result = query.run()
|
|
||||||
ledger_amount = flt(result[0][0]) if result else 0
|
|
||||||
request_paid_amount = flt(result[0][1]) if result else 0
|
|
||||||
|
|
||||||
return ledger_amount - request_paid_amount
|
|
||||||
|
|
||||||
|
|
||||||
def get_gateway_details(args): # nosemgrep
|
def get_gateway_details(args): # nosemgrep
|
||||||
|
|||||||
@@ -313,6 +313,16 @@ class TestPaymentRequest(FrappeTestCase):
|
|||||||
self.assertEqual(pr.outstanding_amount, 800)
|
self.assertEqual(pr.outstanding_amount, 800)
|
||||||
self.assertEqual(pr.grand_total, 1000)
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.exceptions.ValidationError,
|
||||||
|
re.compile(r"Payment Request is already created"),
|
||||||
|
make_payment_request,
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
# complete payment
|
# complete payment
|
||||||
pe = pr.create_payment_entry()
|
pe = pr.create_payment_entry()
|
||||||
|
|
||||||
@@ -331,7 +341,7 @@ class TestPaymentRequest(FrappeTestCase):
|
|||||||
# creating a more payment Request must not allowed
|
# creating a more payment Request must not allowed
|
||||||
self.assertRaisesRegex(
|
self.assertRaisesRegex(
|
||||||
frappe.exceptions.ValidationError,
|
frappe.exceptions.ValidationError,
|
||||||
re.compile(r"Payment Request is already created"),
|
re.compile(r"Payment Entry is already created"),
|
||||||
make_payment_request,
|
make_payment_request,
|
||||||
dt="Sales Order",
|
dt="Sales Order",
|
||||||
dn=so.name,
|
dn=so.name,
|
||||||
@@ -361,6 +371,17 @@ class TestPaymentRequest(FrappeTestCase):
|
|||||||
self.assertEqual(pr.party_account_currency, "INR")
|
self.assertEqual(pr.party_account_currency, "INR")
|
||||||
self.assertEqual(pr.status, "Initiated")
|
self.assertEqual(pr.status, "Initiated")
|
||||||
|
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.exceptions.ValidationError,
|
||||||
|
re.compile(r"Payment Request is already created"),
|
||||||
|
make_payment_request,
|
||||||
|
dt="Purchase Invoice",
|
||||||
|
dn=pi.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
# to make partial payment
|
# to make partial payment
|
||||||
pe = pr.create_payment_entry(submit=False)
|
pe = pr.create_payment_entry(submit=False)
|
||||||
pe.paid_amount = 2000
|
pe.paid_amount = 2000
|
||||||
@@ -389,7 +410,7 @@ class TestPaymentRequest(FrappeTestCase):
|
|||||||
# creating a more payment Request must not allowed
|
# creating a more payment Request must not allowed
|
||||||
self.assertRaisesRegex(
|
self.assertRaisesRegex(
|
||||||
frappe.exceptions.ValidationError,
|
frappe.exceptions.ValidationError,
|
||||||
re.compile(r"Payment Request is already created"),
|
re.compile(r"Payment Entry is already created"),
|
||||||
make_payment_request,
|
make_payment_request,
|
||||||
dt="Purchase Invoice",
|
dt="Purchase Invoice",
|
||||||
dn=pi.name,
|
dn=pi.name,
|
||||||
|
|||||||
@@ -124,6 +124,11 @@ class POSClosingEntry(StatusUpdater):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
consolidate_pos_invoices(closing_entry=self)
|
consolidate_pos_invoices(closing_entry=self)
|
||||||
|
frappe.publish_realtime(
|
||||||
|
f"poe_{self.pos_opening_entry}_closed",
|
||||||
|
self,
|
||||||
|
docname=f"POS Opening Entry/{self.pos_opening_entry}",
|
||||||
|
)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
unconsolidate_pos_invoices(closing_entry=self)
|
unconsolidate_pos_invoices(closing_entry=self)
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
|
|
||||||
# run on validate method of selling controller
|
# run on validate method of selling controller
|
||||||
super(SalesInvoice, self).validate()
|
super(SalesInvoice, self).validate()
|
||||||
|
self.validate_pos_opening_entry()
|
||||||
self.validate_auto_set_posting_time()
|
self.validate_auto_set_posting_time()
|
||||||
self.validate_mode_of_payment()
|
self.validate_mode_of_payment()
|
||||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||||
@@ -320,6 +321,18 @@ class POSInvoice(SalesInvoice):
|
|||||||
_("Payment related to {0} is not completed").format(pay.mode_of_payment)
|
_("Payment related to {0} is not completed").format(pay.mode_of_payment)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_pos_opening_entry(self):
|
||||||
|
opening_entries = frappe.get_list(
|
||||||
|
"POS Opening Entry", filters={"pos_profile": self.pos_profile, "status": "Open", "docstatus": 1}
|
||||||
|
)
|
||||||
|
if len(opening_entries) == 0:
|
||||||
|
frappe.throw(
|
||||||
|
title=_("POS Opening Entry Missing"),
|
||||||
|
msg=_("No open POS Opening Entry found for POS Profile {0}.").format(
|
||||||
|
frappe.bold(self.pos_profile)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def validate_stock_availablility(self):
|
def validate_stock_availablility(self):
|
||||||
if self.is_return:
|
if self.is_return:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
|
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
|
||||||
frappe.db.sql("delete from `tabTax Rule`")
|
frappe.db.sql("delete from `tabTax Rule`")
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||||
|
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
|
||||||
|
|
||||||
|
cls.test_user, cls.pos_profile = init_user_and_profile()
|
||||||
|
create_opening_entry(cls.pos_profile, cls.test_user)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
if frappe.session.user != "Administrator":
|
if frappe.session.user != "Administrator":
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|||||||
@@ -70,3 +70,6 @@ class POSOpeningEntry(StatusUpdater):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.set_status(update=True)
|
self.set_status(update=True)
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
self.set_status(update=True)
|
||||||
|
|||||||
@@ -2094,7 +2094,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
pi = make_pi_from_pr(pr.name)
|
pi = make_pi_from_pr(pr.name)
|
||||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
|
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||||
|
|
||||||
automatically_fetch_payment_terms(enable=0)
|
automatically_fetch_payment_terms(enable=0)
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
@@ -2683,6 +2683,78 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
self.assertRaises(StockOverReturnError, return_doc.save)
|
self.assertRaises(StockOverReturnError, return_doc.save)
|
||||||
|
|
||||||
|
def test_apply_discount_on_grand_total(self):
|
||||||
|
"""
|
||||||
|
To test if after applying discount on grand total,
|
||||||
|
the grand total is calculated correctly without any rounding errors
|
||||||
|
"""
|
||||||
|
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||||
|
invoice.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 21.39,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
invoice.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"rate": 15.5,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# the grand total here will be 255.71
|
||||||
|
invoice.disable_rounded_total = 1
|
||||||
|
# apply discount on grand total to adjust the grand total to 255
|
||||||
|
invoice.discount_amount = 0.71
|
||||||
|
invoice.save()
|
||||||
|
|
||||||
|
# check if grand total is 496 and not something like 254.99 due to rounding errors
|
||||||
|
self.assertEqual(invoice.grand_total, 255)
|
||||||
|
|
||||||
|
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
|
||||||
|
"""
|
||||||
|
To test if after applying discount on grand total,
|
||||||
|
where the tax is calculated on previous row total, the grand total is calculated correctly
|
||||||
|
"""
|
||||||
|
|
||||||
|
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||||
|
invoice.extend(
|
||||||
|
"taxes",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"tax_amount": 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"charge_type": "On Previous Row Amount",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"row_id": 1,
|
||||||
|
"rate": 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"charge_type": "On Previous Row Total",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"row_id": 1,
|
||||||
|
"rate": 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# the total here will be 340, so applying 40 discount
|
||||||
|
invoice.discount_amount = 40
|
||||||
|
invoice.save()
|
||||||
|
|
||||||
|
self.assertEqual(invoice.grand_total, 300)
|
||||||
|
|
||||||
|
|
||||||
def set_advance_flag(company, flag, default_account):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
|||||||
@@ -267,8 +267,8 @@ class SalesInvoice(SellingController):
|
|||||||
self.indicator_title = _("Paid")
|
self.indicator_title = _("Paid")
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
super().validate()
|
|
||||||
self.validate_auto_set_posting_time()
|
self.validate_auto_set_posting_time()
|
||||||
|
super().validate()
|
||||||
|
|
||||||
if not (self.is_pos or self.is_debit_note):
|
if not (self.is_pos or self.is_debit_note):
|
||||||
self.so_dn_required()
|
self.so_dn_required()
|
||||||
|
|||||||
@@ -4284,6 +4284,35 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
pos_return = make_sales_return(pos.name)
|
pos_return = make_sales_return(pos.name)
|
||||||
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
|
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
|
||||||
|
|
||||||
|
def test_create_return_invoice_for_self_update(self):
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
|
|
||||||
|
invoice = create_sales_invoice()
|
||||||
|
|
||||||
|
payment_entry = get_payment_entry(dt=invoice.doctype, dn=invoice.name)
|
||||||
|
payment_entry.reference_no = "test001"
|
||||||
|
payment_entry.reference_date = getdate()
|
||||||
|
|
||||||
|
payment_entry.save()
|
||||||
|
payment_entry.submit()
|
||||||
|
|
||||||
|
r_invoice = make_return_doc(invoice.doctype, invoice.name)
|
||||||
|
|
||||||
|
r_invoice.update_outstanding_for_self = 0
|
||||||
|
r_invoice.save()
|
||||||
|
|
||||||
|
self.assertEqual(r_invoice.update_outstanding_for_self, 1)
|
||||||
|
|
||||||
|
r_invoice.submit()
|
||||||
|
|
||||||
|
self.assertNotEqual(r_invoice.outstanding_amount, 0)
|
||||||
|
|
||||||
|
invoice.reload()
|
||||||
|
|
||||||
|
self.assertEqual(invoice.outstanding_amount, 0)
|
||||||
|
|
||||||
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
|
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
|
||||||
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
|
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
erpnext.utils.add_dimensions("Accounts Payable", 9);
|
erpnext.utils.add_dimensions("Accounts Payable", 10);
|
||||||
|
|
||||||
function get_party_type_options() {
|
function get_party_type_options() {
|
||||||
let options = [];
|
let options = [];
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
item=self.item,
|
item=self.item,
|
||||||
@@ -34,6 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
rate=100,
|
rate=100,
|
||||||
price_list_rate=100,
|
price_list_rate=100,
|
||||||
do_not_save=1,
|
do_not_save=1,
|
||||||
|
**args,
|
||||||
)
|
)
|
||||||
if not no_payment_schedule:
|
if not no_payment_schedule:
|
||||||
si.append(
|
si.append(
|
||||||
@@ -108,7 +109,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
|
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
|
||||||
pos_inv.cancel()
|
pos_inv.cancel()
|
||||||
|
|
||||||
def test_accounts_receivable(self):
|
def test_accounts_receivable_with_payment(self):
|
||||||
filters = {
|
filters = {
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
"based_on_payment_terms": 1,
|
"based_on_payment_terms": 1,
|
||||||
@@ -145,11 +146,15 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||||
cr_note.update_outstanding_for_self = False
|
cr_note.update_outstanding_for_self = False
|
||||||
cr_note.save().submit()
|
cr_note.save().submit()
|
||||||
|
|
||||||
|
# as the invoice partially paid and returning the full amount so the outstanding amount should be True
|
||||||
|
self.assertEqual(cr_note.update_outstanding_for_self, True)
|
||||||
|
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
expected_data_after_credit_note = [0, 0, 100, 0, -100, self.debit_to]
|
||||||
|
|
||||||
row = report[1][0]
|
row = report[1][-1]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
expected_data_after_credit_note,
|
expected_data_after_credit_note,
|
||||||
[
|
[
|
||||||
@@ -162,6 +167,99 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_accounts_receivable_without_payment(self):
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"based_on_payment_terms": 1,
|
||||||
|
"report_date": today(),
|
||||||
|
"range": "30, 60, 90, 120",
|
||||||
|
"show_remarks": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||||
|
si = self.create_sales_invoice()
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
row = report[1][i - 1]
|
||||||
|
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||||
|
|
||||||
|
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||||
|
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||||
|
cr_note.update_outstanding_for_self = False
|
||||||
|
cr_note.save().submit()
|
||||||
|
|
||||||
|
self.assertEqual(cr_note.update_outstanding_for_self, False)
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
row = report[1]
|
||||||
|
self.assertTrue(len(row) == 0)
|
||||||
|
|
||||||
|
def test_accounts_receivable_with_partial_payment(self):
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"based_on_payment_terms": 1,
|
||||||
|
"report_date": today(),
|
||||||
|
"range": "30, 60, 90, 120",
|
||||||
|
"show_remarks": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||||
|
si = self.create_sales_invoice(qty=2)
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]]
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
row = report[1][i - 1]
|
||||||
|
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||||
|
|
||||||
|
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||||
|
self.create_payment_entry(si.name)
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data_after_payment = [[200, 60, 40, 20], [200, 100, 0, 100], [200, 40, 0, 40]]
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
row = report[1][i - 1]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data_after_payment[i - 1],
|
||||||
|
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||||
|
)
|
||||||
|
|
||||||
|
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||||
|
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||||
|
cr_note.update_outstanding_for_self = False
|
||||||
|
cr_note.save().submit()
|
||||||
|
|
||||||
|
self.assertFalse(cr_note.update_outstanding_for_self)
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data_after_credit_note = [
|
||||||
|
[200, 100, 0, 80, 20, self.debit_to],
|
||||||
|
[200, 40, 0, 0, 40, self.debit_to],
|
||||||
|
]
|
||||||
|
|
||||||
|
for i in range(2):
|
||||||
|
row = report[1][i - 1]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data_after_credit_note[i - 1],
|
||||||
|
[
|
||||||
|
row.invoice_grand_total,
|
||||||
|
row.invoiced,
|
||||||
|
row.paid,
|
||||||
|
row.credit_note,
|
||||||
|
row.outstanding,
|
||||||
|
row.party_account,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_cr_note_flag_to_update_self(self):
|
def test_cr_note_flag_to_update_self(self):
|
||||||
filters = {
|
filters = {
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
|
|||||||
@@ -52,11 +52,6 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)");
|
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fieldname: "against_voucher_no",
|
|
||||||
label: __("Against Voucher No"),
|
|
||||||
fieldtype: "Data",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldtype: "Break",
|
fieldtype: "Break",
|
||||||
},
|
},
|
||||||
@@ -66,7 +61,7 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
fieldtype: "Autocomplete",
|
fieldtype: "Autocomplete",
|
||||||
options: Object.keys(frappe.boot.party_account_types),
|
options: Object.keys(frappe.boot.party_account_types),
|
||||||
on_change: function () {
|
on_change: function () {
|
||||||
frappe.query_report.set_filter_value("party", "");
|
frappe.query_report.set_filter_value("party", []);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -224,9 +224,6 @@ def get_conditions(filters):
|
|||||||
if filters.get("voucher_no"):
|
if filters.get("voucher_no"):
|
||||||
conditions.append("voucher_no=%(voucher_no)s")
|
conditions.append("voucher_no=%(voucher_no)s")
|
||||||
|
|
||||||
if filters.get("against_voucher_no"):
|
|
||||||
conditions.append("against_voucher=%(against_voucher_no)s")
|
|
||||||
|
|
||||||
if filters.get("ignore_err"):
|
if filters.get("ignore_err"):
|
||||||
err_journals = frappe.db.get_all(
|
err_journals = frappe.db.get_all(
|
||||||
"Journal Entry",
|
"Journal Entry",
|
||||||
@@ -490,9 +487,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
|
|||||||
data[key][rev_dr_or_cr] = 0
|
data[key][rev_dr_or_cr] = 0
|
||||||
data[key][rev_dr_or_cr + "_in_account_currency"] = 0
|
data[key][rev_dr_or_cr + "_in_account_currency"] = 0
|
||||||
|
|
||||||
if data[key].against_voucher and gle.against_voucher:
|
|
||||||
data[key].against_voucher += ", " + gle.against_voucher
|
|
||||||
|
|
||||||
from_date, to_date = getdate(filters.from_date), getdate(filters.to_date)
|
from_date, to_date = getdate(filters.from_date), getdate(filters.to_date)
|
||||||
show_opening_entries = filters.get("show_opening_entries")
|
show_opening_entries = filters.get("show_opening_entries")
|
||||||
|
|
||||||
@@ -695,14 +689,6 @@ def get_columns(filters):
|
|||||||
|
|
||||||
columns.extend(
|
columns.extend(
|
||||||
[
|
[
|
||||||
{"label": _("Against Voucher Type"), "fieldname": "against_voucher_type", "width": 100},
|
|
||||||
{
|
|
||||||
"label": _("Against Voucher"),
|
|
||||||
"fieldname": "against_voucher",
|
|
||||||
"fieldtype": "Dynamic Link",
|
|
||||||
"options": "against_voucher_type",
|
|
||||||
"width": 100,
|
|
||||||
},
|
|
||||||
{"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100},
|
{"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import erpnext
|
|||||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||||
add_sub_total_row,
|
add_sub_total_row,
|
||||||
add_total_row,
|
add_total_row,
|
||||||
apply_group_by_conditions,
|
apply_order_by_conditions,
|
||||||
get_grand_total,
|
get_grand_total,
|
||||||
get_group_by_and_display_fields,
|
get_group_by_and_display_fields,
|
||||||
get_tax_accounts,
|
get_tax_accounts,
|
||||||
@@ -305,12 +305,6 @@ def apply_conditions(query, pi, pii, filters):
|
|||||||
if filters.get("item_group"):
|
if filters.get("item_group"):
|
||||||
query = query.where(pii.item_group == filters.get("item_group"))
|
query = query.where(pii.item_group == filters.get("item_group"))
|
||||||
|
|
||||||
if not filters.get("group_by"):
|
|
||||||
query = query.orderby(pi.posting_date, order=Order.desc)
|
|
||||||
query = query.orderby(pii.item_group, order=Order.desc)
|
|
||||||
else:
|
|
||||||
query = apply_group_by_conditions(query, pi, pii, filters)
|
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
@@ -372,7 +366,17 @@ def get_items(filters, additional_table_columns):
|
|||||||
|
|
||||||
query = apply_conditions(query, pi, pii, filters)
|
query = apply_conditions(query, pi, pii, filters)
|
||||||
|
|
||||||
return query.run(as_dict=True)
|
from frappe.desk.reportview import build_match_conditions
|
||||||
|
|
||||||
|
query, params = query.walk()
|
||||||
|
match_conditions = build_match_conditions("Sales Invoice")
|
||||||
|
|
||||||
|
if match_conditions:
|
||||||
|
query += " and " + match_conditions
|
||||||
|
|
||||||
|
query = apply_order_by_conditions(query, pi, pii, filters)
|
||||||
|
|
||||||
|
return frappe.db.sql(query, params, as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
def get_aii_accounts():
|
def get_aii_accounts():
|
||||||
|
|||||||
@@ -384,27 +384,24 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
|||||||
| (si.unrealized_profit_loss_account == filters.get("income_account"))
|
| (si.unrealized_profit_loss_account == filters.get("income_account"))
|
||||||
)
|
)
|
||||||
|
|
||||||
if not filters.get("group_by"):
|
|
||||||
query = query.orderby(si.posting_date, order=Order.desc)
|
|
||||||
query = query.orderby(sii.item_group, order=Order.desc)
|
|
||||||
else:
|
|
||||||
query = apply_group_by_conditions(query, si, sii, filters)
|
|
||||||
|
|
||||||
for key, value in (additional_conditions or {}).items():
|
for key, value in (additional_conditions or {}).items():
|
||||||
query = query.where(si[key] == value)
|
query = query.where(si[key] == value)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
def apply_group_by_conditions(query, si, ii, filters):
|
def apply_order_by_conditions(query, si, ii, filters):
|
||||||
if filters.get("group_by") == "Invoice":
|
if not filters.get("group_by"):
|
||||||
query = query.orderby(ii.parent, order=Order.desc)
|
query += f" order by {si.posting_date} desc, {ii.item_group} desc"
|
||||||
|
elif filters.get("group_by") == "Invoice":
|
||||||
|
query += f" order by {ii.parent} desc"
|
||||||
elif filters.get("group_by") == "Item":
|
elif filters.get("group_by") == "Item":
|
||||||
query = query.orderby(ii.item_code)
|
query += f" order by {ii.item_code}"
|
||||||
elif filters.get("group_by") == "Item Group":
|
elif filters.get("group_by") == "Item Group":
|
||||||
query = query.orderby(ii.item_group)
|
query += f" order by {ii.item_group}"
|
||||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||||
query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
|
filter_field = frappe.scrub(filters.get("group_by"))
|
||||||
|
query += f" order by {filter_field} desc"
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
@@ -479,7 +476,17 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
|||||||
|
|
||||||
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||||
|
|
||||||
return query.run(as_dict=True)
|
from frappe.desk.reportview import build_match_conditions
|
||||||
|
|
||||||
|
query, params = query.walk()
|
||||||
|
match_conditions = build_match_conditions("Sales Invoice")
|
||||||
|
|
||||||
|
if match_conditions:
|
||||||
|
query += " and " + match_conditions
|
||||||
|
|
||||||
|
query = apply_order_by_conditions(query, si, sii, filters)
|
||||||
|
|
||||||
|
return frappe.db.sql(query, params, as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
def get_delivery_notes_against_sales_order(item_list):
|
def get_delivery_notes_against_sales_order(item_list):
|
||||||
|
|||||||
@@ -397,7 +397,6 @@ def get_invoices(filters, additional_query_columns):
|
|||||||
pi.mode_of_payment,
|
pi.mode_of_payment,
|
||||||
)
|
)
|
||||||
.where(pi.docstatus == 1)
|
.where(pi.docstatus == 1)
|
||||||
.orderby(pi.posting_date, pi.name, order=Order.desc)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if additional_query_columns:
|
if additional_query_columns:
|
||||||
@@ -421,8 +420,17 @@ def get_invoices(filters, additional_query_columns):
|
|||||||
)
|
)
|
||||||
query = query.where(pi.credit_to.isin(party_account))
|
query = query.where(pi.credit_to.isin(party_account))
|
||||||
|
|
||||||
invoices = query.run(as_dict=True)
|
from frappe.desk.reportview import build_match_conditions
|
||||||
return invoices
|
|
||||||
|
query, params = query.walk()
|
||||||
|
match_conditions = build_match_conditions("Purchase Invoice")
|
||||||
|
|
||||||
|
if match_conditions:
|
||||||
|
query += " and " + match_conditions
|
||||||
|
|
||||||
|
query += " order by posting_date desc, name desc"
|
||||||
|
|
||||||
|
return frappe.db.sql(query, params, as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
def get_conditions(filters, query, doctype):
|
def get_conditions(filters, query, doctype):
|
||||||
|
|||||||
@@ -439,7 +439,6 @@ def get_invoices(filters, additional_query_columns):
|
|||||||
si.company,
|
si.company,
|
||||||
)
|
)
|
||||||
.where(si.docstatus == 1)
|
.where(si.docstatus == 1)
|
||||||
.orderby(si.posting_date, si.name, order=Order.desc)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if additional_query_columns:
|
if additional_query_columns:
|
||||||
@@ -457,8 +456,17 @@ def get_invoices(filters, additional_query_columns):
|
|||||||
filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
|
filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
|
||||||
)
|
)
|
||||||
|
|
||||||
invoices = query.run(as_dict=True)
|
from frappe.desk.reportview import build_match_conditions
|
||||||
return invoices
|
|
||||||
|
query, params = query.walk()
|
||||||
|
match_conditions = build_match_conditions("Sales Invoice")
|
||||||
|
|
||||||
|
if match_conditions:
|
||||||
|
query += " and " + match_conditions
|
||||||
|
|
||||||
|
query += " order by posting_date desc, name desc"
|
||||||
|
|
||||||
|
return frappe.db.sql(query, params, as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
def get_conditions(filters, query, doctype):
|
def get_conditions(filters, query, doctype):
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ class AccountsTestMixin:
|
|||||||
"attribute_name": "bank",
|
"attribute_name": "bank",
|
||||||
"account_name": "HDFC",
|
"account_name": "HDFC",
|
||||||
"parent_account": "Bank Accounts - " + abbr,
|
"parent_account": "Bank Accounts - " + abbr,
|
||||||
|
"account_type": "Bank",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
|
|||||||
@@ -1854,14 +1854,17 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
|||||||
):
|
):
|
||||||
outstanding = voucher_outstanding[0]
|
outstanding = voucher_outstanding[0]
|
||||||
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
||||||
|
outstanding_amount = flt(
|
||||||
|
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
|
||||||
|
)
|
||||||
|
|
||||||
# Didn't use db_set for optimisation purpose
|
# Didn't use db_set for optimisation purpose
|
||||||
ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0
|
ref_doc.outstanding_amount = outstanding_amount
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
voucher_type,
|
voucher_type,
|
||||||
voucher_no,
|
voucher_no,
|
||||||
"outstanding_amount",
|
"outstanding_amount",
|
||||||
outstanding["outstanding_in_account_currency"] or 0.0,
|
outstanding_amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
ref_doc.set_status(update=True)
|
ref_doc.set_status(update=True)
|
||||||
|
|||||||
@@ -621,7 +621,7 @@
|
|||||||
"doc_view": "List",
|
"doc_view": "List",
|
||||||
"label": "Learn Accounting",
|
"label": "Learn Accounting",
|
||||||
"type": "URL",
|
"type": "URL",
|
||||||
"url": "https://frappe.school/courses/erpnext-accounting?utm_source=in_app"
|
"url": "https://school.frappe.io/lms/courses/erpnext-accounting?utm_source=in_app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Chart of Accounts",
|
"label": "Chart of Accounts",
|
||||||
|
|||||||
@@ -22,15 +22,27 @@ frappe.listview_settings["Purchase Order"] = {
|
|||||||
return [
|
return [
|
||||||
__("To Receive and Bill"),
|
__("To Receive and Bill"),
|
||||||
"orange",
|
"orange",
|
||||||
"per_received,<,100|per_billed,<,100|status,!=,Closed",
|
"per_received,<,100|per_billed,<,100|status,!=,Closed|docstatus,=,1",
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return [__("To Receive"), "orange", "per_received,<,100|per_billed,=,100|status,!=,Closed"];
|
return [
|
||||||
|
__("To Receive"),
|
||||||
|
"orange",
|
||||||
|
"per_received,<,100|per_billed,=,100|status,!=,Closed|docstatus,=,1",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
} else if (flt(doc.per_received) >= 100 && flt(doc.per_billed) < 100 && doc.status !== "Closed") {
|
} else if (flt(doc.per_received) >= 100 && flt(doc.per_billed) < 100 && doc.status !== "Closed") {
|
||||||
return [__("To Bill"), "orange", "per_received,=,100|per_billed,<,100|status,!=,Closed"];
|
return [
|
||||||
|
__("To Bill"),
|
||||||
|
"orange",
|
||||||
|
"per_received,=,100|per_billed,<,100|status,!=,Closed|docstatus,=,1",
|
||||||
|
];
|
||||||
} else if (flt(doc.per_received) >= 100 && flt(doc.per_billed) == 100 && doc.status !== "Closed") {
|
} else if (flt(doc.per_received) >= 100 && flt(doc.per_billed) == 100 && doc.status !== "Closed") {
|
||||||
return [__("Completed"), "green", "per_received,=,100|per_billed,=,100|status,!=,Closed"];
|
return [
|
||||||
|
__("Completed"),
|
||||||
|
"green",
|
||||||
|
"per_received,=,100|per_billed,=,100|status,!=,Closed|docstatus,=,1",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onload: function (listview) {
|
onload: function (listview) {
|
||||||
|
|||||||
@@ -537,7 +537,7 @@
|
|||||||
"doc_view": "List",
|
"doc_view": "List",
|
||||||
"label": "Learn Procurement",
|
"label": "Learn Procurement",
|
||||||
"type": "URL",
|
"type": "URL",
|
||||||
"url": "https://frappe.school/courses/procurement?utm_source=in_app"
|
"url": "https://school.frappe.io/lms/courses/procurement?utm_source=in_app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"color": "Yellow",
|
"color": "Yellow",
|
||||||
|
|||||||
@@ -165,6 +165,48 @@ class AccountsController(TransactionBase):
|
|||||||
raise_exception=1,
|
raise_exception=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_against_voucher_outstanding(self):
|
||||||
|
from frappe.model.meta import get_meta
|
||||||
|
|
||||||
|
if not get_meta(self.doctype).has_field("outstanding_amount"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.get("is_return") and self.return_against and not self.get("is_pos"):
|
||||||
|
against_voucher_outstanding = frappe.get_value(
|
||||||
|
self.doctype, self.return_against, "outstanding_amount"
|
||||||
|
)
|
||||||
|
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||||
|
|
||||||
|
msg = ""
|
||||||
|
if self.get("update_outstanding_for_self"):
|
||||||
|
msg = (
|
||||||
|
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, "
|
||||||
|
"uncheck '{2}' checkbox. <br><br>Or"
|
||||||
|
).format(
|
||||||
|
frappe.bold(document_type),
|
||||||
|
get_link_to_form(self.doctype, self.get("return_against")),
|
||||||
|
frappe.bold(_("Update Outstanding for Self")),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif not self.update_outstanding_for_self and (
|
||||||
|
abs(flt(self.rounded_total) or flt(self.grand_total)) > flt(against_voucher_outstanding)
|
||||||
|
):
|
||||||
|
self.update_outstanding_for_self = 1
|
||||||
|
msg = (
|
||||||
|
"The outstanding amount {} in {} is lesser than {}. Updating the outstanding to this invoice. <br><br>And"
|
||||||
|
).format(
|
||||||
|
against_voucher_outstanding,
|
||||||
|
get_link_to_form(self.doctype, self.get("return_against")),
|
||||||
|
flt(abs(self.outstanding_amount)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if msg:
|
||||||
|
msg += " you can use {} tool to reconcile against {} later.".format(
|
||||||
|
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
||||||
|
get_link_to_form(self.doctype, self.get("return_against")),
|
||||||
|
)
|
||||||
|
frappe.msgprint(_(msg))
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if not self.get("is_return") and not self.get("is_debit_note"):
|
if not self.get("is_return") and not self.get("is_debit_note"):
|
||||||
self.validate_qty_is_not_zero()
|
self.validate_qty_is_not_zero()
|
||||||
@@ -193,6 +235,7 @@ class AccountsController(TransactionBase):
|
|||||||
self.disable_tax_included_prices_for_internal_transfer()
|
self.disable_tax_included_prices_for_internal_transfer()
|
||||||
self.set_incoming_rate()
|
self.set_incoming_rate()
|
||||||
self.init_internal_values()
|
self.init_internal_values()
|
||||||
|
self.validate_against_voucher_outstanding()
|
||||||
|
|
||||||
# Need to set taxes based on taxes_and_charges template
|
# Need to set taxes based on taxes_and_charges template
|
||||||
# before calculating taxes and totals
|
# before calculating taxes and totals
|
||||||
@@ -228,20 +271,6 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
|
|
||||||
if self.get("update_outstanding_for_self"):
|
|
||||||
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
|
||||||
frappe.msgprint(
|
|
||||||
_(
|
|
||||||
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox. <br><br> Or you can use {3} tool to reconcile against {1} later."
|
|
||||||
).format(
|
|
||||||
frappe.bold(document_type),
|
|
||||||
get_link_to_form(self.doctype, self.get("return_against")),
|
|
||||||
frappe.bold(_("Update Outstanding for Self")),
|
|
||||||
get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||||
self.set_advances()
|
self.set_advances()
|
||||||
@@ -2328,7 +2357,9 @@ class AccountsController(TransactionBase):
|
|||||||
and automatically_fetch_payment_terms
|
and automatically_fetch_payment_terms
|
||||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||||
):
|
):
|
||||||
self.fetch_payment_terms_from_order(po_or_so, doctype)
|
self.fetch_payment_terms_from_order(
|
||||||
|
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||||
|
)
|
||||||
if self.get("payment_terms_template"):
|
if self.get("payment_terms_template"):
|
||||||
self.ignore_default_payment_terms_template = 1
|
self.ignore_default_payment_terms_template = 1
|
||||||
elif self.get("payment_terms_template"):
|
elif self.get("payment_terms_template"):
|
||||||
@@ -2372,7 +2403,9 @@ class AccountsController(TransactionBase):
|
|||||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.fetch_payment_terms_from_order(po_or_so, doctype)
|
self.fetch_payment_terms_from_order(
|
||||||
|
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||||
|
)
|
||||||
self.ignore_default_payment_terms_template = 1
|
self.ignore_default_payment_terms_template = 1
|
||||||
|
|
||||||
def get_order_details(self):
|
def get_order_details(self):
|
||||||
@@ -2410,7 +2443,9 @@ class AccountsController(TransactionBase):
|
|||||||
def linked_order_has_payment_schedule(self, po_or_so):
|
def linked_order_has_payment_schedule(self, po_or_so):
|
||||||
return frappe.get_all("Payment Schedule", filters={"parent": po_or_so})
|
return frappe.get_all("Payment Schedule", filters={"parent": po_or_so})
|
||||||
|
|
||||||
def fetch_payment_terms_from_order(self, po_or_so, po_or_so_doctype):
|
def fetch_payment_terms_from_order(
|
||||||
|
self, po_or_so, po_or_so_doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice.
|
Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice.
|
||||||
"""
|
"""
|
||||||
@@ -2426,12 +2461,25 @@ class AccountsController(TransactionBase):
|
|||||||
"invoice_portion": schedule.invoice_portion,
|
"invoice_portion": schedule.invoice_portion,
|
||||||
"mode_of_payment": schedule.mode_of_payment,
|
"mode_of_payment": schedule.mode_of_payment,
|
||||||
"description": schedule.description,
|
"description": schedule.description,
|
||||||
"payment_amount": schedule.payment_amount,
|
|
||||||
"base_payment_amount": schedule.base_payment_amount,
|
|
||||||
"outstanding": schedule.outstanding,
|
|
||||||
"paid_amount": schedule.paid_amount,
|
"paid_amount": schedule.paid_amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if automatically_fetch_payment_terms:
|
||||||
|
payment_schedule["payment_amount"] = flt(
|
||||||
|
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||||
|
schedule.precision("payment_amount"),
|
||||||
|
)
|
||||||
|
payment_schedule["base_payment_amount"] = flt(
|
||||||
|
base_grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||||
|
schedule.precision("base_payment_amount"),
|
||||||
|
)
|
||||||
|
payment_schedule["outstanding"] = payment_schedule["payment_amount"]
|
||||||
|
else:
|
||||||
|
payment_schedule["base_payment_amount"] = flt(
|
||||||
|
schedule.base_payment_amount * self.get("conversion_rate"),
|
||||||
|
schedule.precision("base_payment_amount"),
|
||||||
|
)
|
||||||
|
|
||||||
if schedule.discount_type == "Percentage":
|
if schedule.discount_type == "Percentage":
|
||||||
payment_schedule["discount_type"] = schedule.discount_type
|
payment_schedule["discount_type"] = schedule.discount_type
|
||||||
payment_schedule["discount"] = schedule.discount
|
payment_schedule["discount"] = schedule.discount
|
||||||
|
|||||||
@@ -898,3 +898,32 @@ def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters)
|
|||||||
)
|
)
|
||||||
|
|
||||||
return query.run(as_dict=False)
|
return query.run(as_dict=False)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
|
def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
if frappe.db.get_single_value("Stock Settings", "allow_uom_with_conversion_rate_defined_in_item"):
|
||||||
|
query_filters = {"parent": filters.get("item_code")}
|
||||||
|
|
||||||
|
if txt:
|
||||||
|
query_filters["uom"] = ["like", f"%{txt}%"]
|
||||||
|
|
||||||
|
return frappe.get_all(
|
||||||
|
"UOM Conversion Detail",
|
||||||
|
filters=query_filters,
|
||||||
|
fields=["uom", "conversion_factor"],
|
||||||
|
limit_start=start,
|
||||||
|
limit_page_length=page_len,
|
||||||
|
order_by="idx",
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return frappe.get_all(
|
||||||
|
"UOM",
|
||||||
|
filters={"name": ["like", f"%{txt}%"]},
|
||||||
|
fields=["name"],
|
||||||
|
limit_start=start,
|
||||||
|
limit_page_length=page_len,
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
|||||||
@@ -774,7 +774,7 @@ class StockController(AccountsController):
|
|||||||
if row.get("batch_no"):
|
if row.get("batch_no"):
|
||||||
update_values["batch_no"] = None
|
update_values["batch_no"] = None
|
||||||
|
|
||||||
if row.serial_and_batch_bundle:
|
if row.get("serial_and_batch_bundle"):
|
||||||
update_values["serial_and_batch_bundle"] = None
|
update_values["serial_and_batch_bundle"] = None
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Serial and Batch Bundle", row.serial_and_batch_bundle, {"is_cancelled": 1}
|
"Serial and Batch Bundle", row.serial_and_batch_bundle, {"is_cancelled": 1}
|
||||||
@@ -1631,6 +1631,8 @@ def is_reposting_pending():
|
|||||||
|
|
||||||
|
|
||||||
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||||
|
from erpnext.stock.utils import get_combine_datetime
|
||||||
|
|
||||||
if allow_force_reposting and frappe.db.get_single_value(
|
if allow_force_reposting and frappe.db.get_single_value(
|
||||||
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||||
):
|
):
|
||||||
@@ -1652,14 +1654,15 @@ def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
|||||||
|
|
||||||
or_conditions = get_conditions_to_validate_future_sle(sl_entries)
|
or_conditions = get_conditions_to_validate_future_sle(sl_entries)
|
||||||
|
|
||||||
|
args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
|
||||||
|
|
||||||
data = frappe.db.sql(
|
data = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select item_code, warehouse, count(name) as total_row
|
select item_code, warehouse, count(name) as total_row
|
||||||
from `tabStock Ledger Entry` force index (item_warehouse)
|
from `tabStock Ledger Entry`
|
||||||
where
|
where
|
||||||
({})
|
({})
|
||||||
and timestamp(posting_date, posting_time)
|
and posting_datetime >= %(posting_datetime)s
|
||||||
>= timestamp(%(posting_date)s, %(posting_time)s)
|
|
||||||
and voucher_no != %(voucher_no)s
|
and voucher_no != %(voucher_no)s
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
GROUP BY
|
GROUP BY
|
||||||
|
|||||||
@@ -743,7 +743,9 @@ class SubcontractingController(StockController):
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.doctype == self.subcontract_data.order_doctype or self.backflush_based_on == "BOM":
|
if self.doctype == self.subcontract_data.order_doctype or (
|
||||||
|
self.backflush_based_on == "BOM" or self.is_return
|
||||||
|
):
|
||||||
for bom_item in self.__get_materials_from_bom(
|
for bom_item in self.__get_materials_from_bom(
|
||||||
row.item_code, row.bom, row.get("include_exploded_items")
|
row.item_code, row.bom, row.get("include_exploded_items")
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -377,9 +377,7 @@ class calculate_taxes_and_totals:
|
|||||||
self._calculate()
|
self._calculate()
|
||||||
|
|
||||||
def calculate_taxes(self):
|
def calculate_taxes(self):
|
||||||
rounding_adjustment_computed = self.doc.get("is_consolidated") and self.doc.get("rounding_adjustment")
|
self.grand_total_diff = 0
|
||||||
if not rounding_adjustment_computed:
|
|
||||||
self.doc.rounding_adjustment = 0
|
|
||||||
|
|
||||||
# maintain actual tax rate based on idx
|
# maintain actual tax rate based on idx
|
||||||
actual_tax_dict = dict(
|
actual_tax_dict = dict(
|
||||||
@@ -446,9 +444,8 @@ class calculate_taxes_and_totals:
|
|||||||
and self.discount_amount_applied
|
and self.discount_amount_applied
|
||||||
and self.doc.discount_amount
|
and self.doc.discount_amount
|
||||||
and self.doc.apply_discount_on == "Grand Total"
|
and self.doc.apply_discount_on == "Grand Total"
|
||||||
and not rounding_adjustment_computed
|
|
||||||
):
|
):
|
||||||
self.doc.rounding_adjustment = flt(
|
self.grand_total_diff = flt(
|
||||||
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
|
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
|
||||||
self.doc.precision("rounding_adjustment"),
|
self.doc.precision("rounding_adjustment"),
|
||||||
)
|
)
|
||||||
@@ -552,11 +549,11 @@ class calculate_taxes_and_totals:
|
|||||||
return self.adjust_grand_total_for_inclusive_tax()
|
return self.adjust_grand_total_for_inclusive_tax()
|
||||||
|
|
||||||
def adjust_grand_total_for_inclusive_tax(self):
|
def adjust_grand_total_for_inclusive_tax(self):
|
||||||
# if fully inclusive taxes and diff
|
# if any inclusive taxes and diff
|
||||||
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
|
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
|
||||||
last_tax = self.doc.get("taxes")[-1]
|
last_tax = self.doc.get("taxes")[-1]
|
||||||
non_inclusive_tax_amount = sum(
|
non_inclusive_tax_amount = sum(
|
||||||
flt(d.tax_amount_after_discount_amount)
|
self.get_tax_amount_if_for_valuation_or_deduction(d.tax_amount_after_discount_amount, d)
|
||||||
for d in self.doc.get("taxes")
|
for d in self.doc.get("taxes")
|
||||||
if not d.included_in_print_rate
|
if not d.included_in_print_rate
|
||||||
)
|
)
|
||||||
@@ -573,27 +570,23 @@ class calculate_taxes_and_totals:
|
|||||||
diff = flt(diff, self.doc.precision("rounding_adjustment"))
|
diff = flt(diff, self.doc.precision("rounding_adjustment"))
|
||||||
|
|
||||||
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
|
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
|
||||||
self.doc.grand_total_diff = diff
|
self.grand_total_diff = diff
|
||||||
else:
|
|
||||||
self.doc.grand_total_diff = 0
|
|
||||||
|
|
||||||
def calculate_totals(self):
|
def calculate_totals(self):
|
||||||
if self.doc.get("taxes"):
|
if self.doc.get("taxes"):
|
||||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(
|
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff
|
||||||
self.doc.get("grand_total_diff")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.doc.grand_total = flt(self.doc.net_total)
|
self.doc.grand_total = flt(self.doc.net_total)
|
||||||
|
|
||||||
if self.doc.get("taxes"):
|
if self.doc.get("taxes"):
|
||||||
self.doc.total_taxes_and_charges = flt(
|
self.doc.total_taxes_and_charges = flt(
|
||||||
self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")),
|
self.doc.grand_total - self.doc.net_total - self.grand_total_diff,
|
||||||
self.doc.precision("total_taxes_and_charges"),
|
self.doc.precision("total_taxes_and_charges"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.doc.total_taxes_and_charges = 0.0
|
self.doc.total_taxes_and_charges = 0.0
|
||||||
|
|
||||||
self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
|
self._set_in_company_currency(self.doc, ["total_taxes_and_charges"])
|
||||||
|
|
||||||
if self.doc.doctype in [
|
if self.doc.doctype in [
|
||||||
"Quotation",
|
"Quotation",
|
||||||
@@ -643,7 +636,9 @@ class calculate_taxes_and_totals:
|
|||||||
|
|
||||||
if self.doc.meta.get_field("rounded_total"):
|
if self.doc.meta.get_field("rounded_total"):
|
||||||
if self.doc.is_rounded_total_disabled():
|
if self.doc.is_rounded_total_disabled():
|
||||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
self.doc.rounded_total = 0
|
||||||
|
self.doc.base_rounded_total = 0
|
||||||
|
self.doc.rounding_adjustment = 0
|
||||||
return
|
return
|
||||||
|
|
||||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
|
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
|
||||||
@@ -687,33 +682,29 @@ class calculate_taxes_and_totals:
|
|||||||
return
|
return
|
||||||
|
|
||||||
total_for_discount_amount = self.get_total_for_discount_amount()
|
total_for_discount_amount = self.get_total_for_discount_amount()
|
||||||
taxes = self.doc.get("taxes")
|
|
||||||
net_total = 0
|
net_total = 0
|
||||||
|
expected_net_total = 0
|
||||||
|
|
||||||
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._items):
|
for item in 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
|
||||||
)
|
)
|
||||||
|
|
||||||
item.net_amount = flt(item.net_amount - distributed_amount, item.precision("net_amount"))
|
adjusted_net_amount = item.net_amount - distributed_amount
|
||||||
|
expected_net_total += adjusted_net_amount
|
||||||
|
item.net_amount = flt(adjusted_net_amount, item.precision("net_amount"))
|
||||||
net_total += item.net_amount
|
net_total += item.net_amount
|
||||||
|
|
||||||
# discount amount rounding loss adjustment if no taxes
|
# discount amount rounding adjustment
|
||||||
if (
|
if rounding_difference := flt(
|
||||||
self.doc.apply_discount_on == "Net Total"
|
expected_net_total - net_total, self.doc.precision("net_total")
|
||||||
or not taxes
|
):
|
||||||
or total_for_discount_amount == self.doc.net_total
|
|
||||||
) and i == len(self._items) - 1:
|
|
||||||
discount_amount_loss = flt(
|
|
||||||
self.doc.net_total - net_total - self.doc.discount_amount,
|
|
||||||
self.doc.precision("net_total"),
|
|
||||||
)
|
|
||||||
|
|
||||||
item.net_amount = flt(
|
item.net_amount = flt(
|
||||||
item.net_amount + discount_amount_loss, item.precision("net_amount")
|
item.net_amount + rounding_difference, item.precision("net_amount")
|
||||||
)
|
)
|
||||||
|
net_total += rounding_difference
|
||||||
|
|
||||||
item.net_rate = (
|
item.net_rate = (
|
||||||
flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0
|
flt(item.net_amount / item.qty, item.precision("net_rate")) if item.qty else 0
|
||||||
@@ -729,20 +720,44 @@ class calculate_taxes_and_totals:
|
|||||||
def get_total_for_discount_amount(self):
|
def get_total_for_discount_amount(self):
|
||||||
if self.doc.apply_discount_on == "Net Total":
|
if self.doc.apply_discount_on == "Net Total":
|
||||||
return self.doc.net_total
|
return self.doc.net_total
|
||||||
else:
|
|
||||||
actual_taxes_dict = {}
|
|
||||||
|
|
||||||
for tax in self.doc.get("taxes"):
|
total_actual_tax = 0
|
||||||
if tax.charge_type in ["Actual", "On Item Quantity"]:
|
actual_taxes_dict = {}
|
||||||
tax_amount = self.get_tax_amount_if_for_valuation_or_deduction(tax.tax_amount, tax)
|
|
||||||
actual_taxes_dict.setdefault(tax.idx, tax_amount)
|
|
||||||
elif tax.row_id in actual_taxes_dict:
|
|
||||||
actual_tax_amount = flt(actual_taxes_dict.get(tax.row_id, 0)) * flt(tax.rate) / 100
|
|
||||||
actual_taxes_dict.setdefault(tax.idx, actual_tax_amount)
|
|
||||||
|
|
||||||
return flt(
|
def update_actual_tax_dict(tax, tax_amount):
|
||||||
self.doc.grand_total - sum(actual_taxes_dict.values()), self.doc.precision("grand_total")
|
nonlocal total_actual_tax
|
||||||
|
|
||||||
|
if tax.get("add_deduct_tax") == "Deduct":
|
||||||
|
tax_amount *= -1
|
||||||
|
|
||||||
|
if tax.get("category") != "Valuation":
|
||||||
|
total_actual_tax += tax_amount
|
||||||
|
|
||||||
|
actual_taxes_dict[int(tax.idx)] = {
|
||||||
|
"tax_amount": tax_amount,
|
||||||
|
"cumulative_tax_amount": total_actual_tax,
|
||||||
|
}
|
||||||
|
|
||||||
|
for tax in self.doc.get("taxes"):
|
||||||
|
if tax.charge_type in ["Actual", "On Item Quantity"]:
|
||||||
|
update_actual_tax_dict(tax, tax.tax_amount)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not tax.row_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
base_row = actual_taxes_dict.get(int(tax.row_id))
|
||||||
|
if not base_row:
|
||||||
|
continue
|
||||||
|
|
||||||
|
base_tax_amount = (
|
||||||
|
base_row["tax_amount"]
|
||||||
|
if tax.charge_type == "On Previous Row Amount"
|
||||||
|
else base_row["cumulative_tax_amount"]
|
||||||
)
|
)
|
||||||
|
update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100)
|
||||||
|
|
||||||
|
return self.doc.grand_total - total_actual_tax
|
||||||
|
|
||||||
def calculate_total_advance(self):
|
def calculate_total_advance(self):
|
||||||
if not self.doc.docstatus.is_cancelled():
|
if not self.doc.docstatus.is_cancelled():
|
||||||
@@ -804,9 +819,12 @@ class calculate_taxes_and_totals:
|
|||||||
if (
|
if (
|
||||||
self.doc.is_return
|
self.doc.is_return
|
||||||
and self.doc.return_against
|
and self.doc.return_against
|
||||||
|
and not self.doc.update_outstanding_for_self
|
||||||
and not self.doc.get("is_pos")
|
and not self.doc.get("is_pos")
|
||||||
or self.is_internal_invoice()
|
or self.is_internal_invoice()
|
||||||
):
|
):
|
||||||
|
# Do not calculate the outstanding amount for a return invoice if 'update_outstanding_for_self' is not enabled.
|
||||||
|
self.doc.outstanding_amount = 0
|
||||||
return
|
return
|
||||||
|
|
||||||
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
|
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
|
||||||
|
|||||||
@@ -1496,11 +1496,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
fields = ["name", "item_name", "item_group", "description"]
|
fields = ["name", "item_name", "item_group", "description"]
|
||||||
fields.extend([field for field in searchfields if field not in ["name", "item_group", "description"]])
|
fields.extend([field for field in searchfields if field not in ["name", "item_group", "description"]])
|
||||||
|
|
||||||
searchfields = searchfields + [
|
if not searchfields:
|
||||||
field
|
searchfields = ["name"]
|
||||||
for field in [searchfield or "name", "item_code", "item_group", "item_name"]
|
|
||||||
if field not in searchfields
|
|
||||||
]
|
|
||||||
|
|
||||||
query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())}
|
query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())}
|
||||||
|
|
||||||
|
|||||||
@@ -1616,7 +1616,7 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
|
|||||||
"posting_date": nowdate(),
|
"posting_date": nowdate(),
|
||||||
"for_quantity": row.job_card_qty or work_order.get("qty", 0),
|
"for_quantity": row.job_card_qty or work_order.get("qty", 0),
|
||||||
"operation_id": row.get("name"),
|
"operation_id": row.get("name"),
|
||||||
"bom_no": row.get("bom"),
|
"bom_no": row.get("bom") or work_order.bom_no,
|
||||||
"project": work_order.project,
|
"project": work_order.project,
|
||||||
"company": work_order.company,
|
"company": work_order.company,
|
||||||
"sequence_id": row.get("sequence_id"),
|
"sequence_id": row.get("sequence_id"),
|
||||||
|
|||||||
@@ -336,7 +336,7 @@
|
|||||||
"doc_view": "List",
|
"doc_view": "List",
|
||||||
"label": "Learn Manufacturing",
|
"label": "Learn Manufacturing",
|
||||||
"type": "URL",
|
"type": "URL",
|
||||||
"url": "https://frappe.school/courses/manufacturing?utm_source=in_app"
|
"url": "https://school.frappe.io/lms/courses/manufacturing?utm_source=in_app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"color": "Grey",
|
"color": "Grey",
|
||||||
|
|||||||
@@ -210,7 +210,7 @@
|
|||||||
"doc_view": "List",
|
"doc_view": "List",
|
||||||
"label": "Learn Project Management",
|
"label": "Learn Project Management",
|
||||||
"type": "URL",
|
"type": "URL",
|
||||||
"url": "https://frappe.school/courses/project-management?utm_source=in_app"
|
"url": "https://school.frappe.io/lms/courses/project-management?utm_source=in_app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"color": "Blue",
|
"color": "Blue",
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
|
|
||||||
calculate_taxes() {
|
calculate_taxes() {
|
||||||
var me = this;
|
var me = this;
|
||||||
this.frm.doc.rounding_adjustment = 0;
|
this.grand_total_diff = 0;
|
||||||
var actual_tax_dict = {};
|
var actual_tax_dict = {};
|
||||||
|
|
||||||
// maintain actual tax rate based on idx
|
// maintain actual tax rate based on idx
|
||||||
@@ -417,7 +417,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
// adjust Discount Amount loss in last tax iteration
|
// adjust Discount Amount loss in last tax iteration
|
||||||
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
|
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
|
||||||
&& me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) {
|
&& me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) {
|
||||||
me.frm.doc.rounding_adjustment = flt(me.frm.doc.grand_total -
|
me.grand_total_diff = flt(me.frm.doc.grand_total -
|
||||||
flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment"));
|
flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -535,7 +535,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
|
|
||||||
adjust_grand_total_for_inclusive_tax() {
|
adjust_grand_total_for_inclusive_tax() {
|
||||||
var me = this;
|
var me = this;
|
||||||
// if fully inclusive taxes and diff
|
|
||||||
|
// if any inclusive taxes and diff
|
||||||
if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
|
if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
|
||||||
var any_inclusive_tax = false;
|
var any_inclusive_tax = false;
|
||||||
$.each(this.frm.doc.taxes || [], function(i, d) {
|
$.each(this.frm.doc.taxes || [], function(i, d) {
|
||||||
@@ -546,7 +547,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
var non_inclusive_tax_amount = frappe.utils.sum($.map(this.frm.doc.taxes || [],
|
var non_inclusive_tax_amount = frappe.utils.sum($.map(this.frm.doc.taxes || [],
|
||||||
function(d) {
|
function(d) {
|
||||||
if(!d.included_in_print_rate) {
|
if(!d.included_in_print_rate) {
|
||||||
return flt(d.tax_amount_after_discount_amount);
|
let tax_amount = d.category === "Valuation" ? 0 : d.tax_amount_after_discount_amount;
|
||||||
|
if (d.add_deduct_tax === "Deduct") tax_amount *= -1;
|
||||||
|
return tax_amount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
@@ -560,9 +563,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
diff = flt(diff, precision("rounding_adjustment"));
|
diff = flt(diff, precision("rounding_adjustment"));
|
||||||
|
|
||||||
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
|
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
|
||||||
me.frm.doc.grand_total_diff = diff;
|
me.grand_total_diff = diff;
|
||||||
} else {
|
|
||||||
me.frm.doc.grand_total_diff = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -573,7 +574,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
var me = this;
|
var me = this;
|
||||||
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
|
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
|
||||||
this.frm.doc.grand_total = flt(tax_count
|
this.frm.doc.grand_total = flt(tax_count
|
||||||
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.grand_total_diff)
|
? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff
|
||||||
: this.frm.doc.net_total);
|
: this.frm.doc.net_total);
|
||||||
|
|
||||||
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
||||||
@@ -605,9 +606,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
|
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
|
||||||
- flt(this.frm.doc.rounding_adjustment), precision("total_taxes_and_charges"));
|
- this.grand_total_diff, precision("total_taxes_and_charges"));
|
||||||
|
|
||||||
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges", "rounding_adjustment"]);
|
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
|
||||||
|
|
||||||
// Round grand total as per precision
|
// Round grand total as per precision
|
||||||
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "base_grand_total"]);
|
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "base_grand_total"]);
|
||||||
@@ -627,6 +628,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
if (cint(disable_rounded_total)) {
|
if (cint(disable_rounded_total)) {
|
||||||
this.frm.doc.rounded_total = 0;
|
this.frm.doc.rounded_total = 0;
|
||||||
this.frm.doc.base_rounded_total = 0;
|
this.frm.doc.base_rounded_total = 0;
|
||||||
|
this.frm.doc.rounding_adjustment = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,22 +697,26 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var total_for_discount_amount = this.get_total_for_discount_amount();
|
const total_for_discount_amount = this.get_total_for_discount_amount();
|
||||||
var net_total = 0;
|
let net_total = 0;
|
||||||
|
let expected_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._items || [], function(i, item) {
|
$.each(this.frm._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, precision("net_amount", item));
|
|
||||||
|
const adjusted_net_amount = item.net_amount - distributed_amount;
|
||||||
|
expected_net_total += adjusted_net_amount
|
||||||
|
item.net_amount = flt(adjusted_net_amount, precision("net_amount", item));
|
||||||
net_total += item.net_amount;
|
net_total += item.net_amount;
|
||||||
|
|
||||||
// discount amount rounding loss adjustment if no taxes
|
// discount amount rounding adjustment
|
||||||
if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total"))
|
// assignment to rounding_difference is intentional
|
||||||
&& i == (me.frm._items || []).length - 1) {
|
const rounding_difference = flt(expected_net_total - net_total, precision("net_total"));
|
||||||
var discount_amount_loss = flt(me.frm.doc.net_total - net_total
|
if (rounding_difference) {
|
||||||
- me.frm.doc.discount_amount, precision("net_total"));
|
item.net_amount = flt(item.net_amount + rounding_difference, precision("net_amount", item));
|
||||||
item.net_amount = flt(item.net_amount + discount_amount_loss,
|
net_total += rounding_difference;
|
||||||
precision("net_amount", item));
|
|
||||||
}
|
}
|
||||||
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
|
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
|
||||||
me.set_in_company_currency(item, ["net_rate", "net_amount"]);
|
me.set_in_company_currency(item, ["net_rate", "net_amount"]);
|
||||||
@@ -723,29 +729,38 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get_total_for_discount_amount() {
|
get_total_for_discount_amount() {
|
||||||
if(this.frm.doc.apply_discount_on == "Net Total") {
|
if(this.frm.doc.apply_discount_on == "Net Total")
|
||||||
return this.frm.doc.net_total;
|
return this.frm.doc.net_total;
|
||||||
} else {
|
|
||||||
var total_actual_tax = 0.0;
|
|
||||||
var actual_taxes_dict = {};
|
|
||||||
|
|
||||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
let total_actual_tax = 0.0;
|
||||||
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
|
let actual_taxes_dict = {};
|
||||||
var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount;
|
|
||||||
tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
|
|
||||||
actual_taxes_dict[tax.idx] = tax_amount;
|
|
||||||
} else if (actual_taxes_dict[tax.row_id] !== null) {
|
|
||||||
var actual_tax_amount = flt(actual_taxes_dict[tax.row_id]) * flt(tax.rate) / 100;
|
|
||||||
actual_taxes_dict[tax.idx] = actual_tax_amount;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$.each(actual_taxes_dict, function(key, value) {
|
function update_actual_taxes_dict(tax, tax_amount) {
|
||||||
if (value) total_actual_tax += value;
|
if (tax.add_deduct_tax == "Deduct") tax_amount *= -1;
|
||||||
});
|
if (tax.category != "Valuation") total_actual_tax += tax_amount;
|
||||||
|
|
||||||
return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total"));
|
actual_taxes_dict[tax.idx] = {
|
||||||
|
tax_amount: tax_amount,
|
||||||
|
cumulative_total: total_actual_tax
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||||
|
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
|
||||||
|
update_actual_taxes_dict(tax, tax.tax_amount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base_row = actual_taxes_dict[tax.row_id];
|
||||||
|
if (!base_row) return;
|
||||||
|
|
||||||
|
// if charge type is 'On Previous Row Amount', calculate tax on previous row amount
|
||||||
|
// else (On Previous Row Total) calculate tax on cumulative total
|
||||||
|
const base_tax_amount = tax.charge_type == "On Previous Row Amount" ? base_row["tax_amount"]: base_row["cumulative_total"];
|
||||||
|
update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.frm.doc.grand_total - total_actual_tax;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculate_total_advance(update_paid_amount) {
|
calculate_total_advance(update_paid_amount) {
|
||||||
|
|||||||
@@ -150,6 +150,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.frm.fields_dict["items"].grid.get_field("uom")) {
|
||||||
|
this.frm.set_query("uom", "items", function(doc, cdt, cdn) {
|
||||||
|
let row = locals[cdt][cdn];
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: "erpnext.controllers.queries.get_item_uom_query",
|
||||||
|
filters: {
|
||||||
|
"item_code": row.item_code
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if(
|
if(
|
||||||
this.frm.docstatus < 2
|
this.frm.docstatus < 2
|
||||||
&& this.frm.fields_dict["payment_terms_template"]
|
&& this.frm.fields_dict["payment_terms_template"]
|
||||||
@@ -238,6 +251,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use_serial_batch_fields(frm, cdt, cdn) {
|
||||||
|
const item = locals[cdt][cdn];
|
||||||
|
if (!item.use_serial_batch_fields) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "serial_no", "");
|
||||||
|
frappe.model.set_value(cdt, cdn, "batch_no", "");
|
||||||
|
frappe.model.set_value(cdt, cdn, "rejected_serial_no", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
set_fields_onload_for_line_item() {
|
set_fields_onload_for_line_item() {
|
||||||
if (this.frm.is_new() && this.frm.doc?.items) {
|
if (this.frm.is_new() && this.frm.doc?.items) {
|
||||||
this.frm.doc.items.forEach(item => {
|
this.frm.doc.items.forEach(item => {
|
||||||
|
|||||||
@@ -263,6 +263,10 @@ $.extend(erpnext.utils, {
|
|||||||
fieldname: dimension["fieldname"],
|
fieldname: dimension["fieldname"],
|
||||||
label: __(dimension["doctype"]),
|
label: __(dimension["doctype"]),
|
||||||
fieldtype: "MultiSelectList",
|
fieldtype: "MultiSelectList",
|
||||||
|
depends_on:
|
||||||
|
report_name === "Stock Balance"
|
||||||
|
? "eval:doc.show_dimension_wise_stock === 1"
|
||||||
|
: "",
|
||||||
get_data: function (txt) {
|
get_data: function (txt) {
|
||||||
return frappe.db.get_link_options(dimension["doctype"], txt);
|
return frappe.db.get_link_options(dimension["doctype"], txt);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -91,7 +91,13 @@ erpnext.accounts.unreconcile_payment = {
|
|||||||
read_only: 1,
|
read_only: 1,
|
||||||
options: "account_currency",
|
options: "account_currency",
|
||||||
},
|
},
|
||||||
{ label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1 },
|
{
|
||||||
|
label: __("Currency"),
|
||||||
|
fieldname: "account_currency",
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Currency",
|
||||||
|
read_only: 1,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let unreconcile_dialog_fields = [
|
let unreconcile_dialog_fields = [
|
||||||
{
|
{
|
||||||
@@ -121,10 +127,10 @@ erpnext.accounts.unreconcile_payment = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let d = new frappe.ui.Dialog({
|
let d = new frappe.ui.Dialog({
|
||||||
title: "UnReconcile Allocations",
|
title: __("UnReconcile Allocations"),
|
||||||
fields: unreconcile_dialog_fields,
|
fields: unreconcile_dialog_fields,
|
||||||
size: "large",
|
size: "large",
|
||||||
primary_action_label: "UnReconcile",
|
primary_action_label: __("UnReconcile"),
|
||||||
primary_action(values) {
|
primary_action(values) {
|
||||||
let selected_allocations = values.allocations.filter((x) => x.__checked);
|
let selected_allocations = values.allocations.filter((x) => x.__checked);
|
||||||
if (selected_allocations.length > 0) {
|
if (selected_allocations.length > 0) {
|
||||||
@@ -138,7 +144,7 @@ erpnext.accounts.unreconcile_payment = {
|
|||||||
);
|
);
|
||||||
d.hide();
|
d.hide();
|
||||||
} else {
|
} else {
|
||||||
frappe.msgprint("No Selection");
|
frappe.msgprint(__("No Selection"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ frappe.ui.form.on("Customer", {
|
|||||||
|
|
||||||
frm.add_fetch("lead_name", "company_name", "customer_name");
|
frm.add_fetch("lead_name", "company_name", "customer_name");
|
||||||
frm.add_fetch("default_sales_partner", "commission_rate", "default_commission_rate");
|
frm.add_fetch("default_sales_partner", "commission_rate", "default_commission_rate");
|
||||||
frm.set_query("customer_group", { is_group: 0 });
|
|
||||||
frm.set_query("default_price_list", { selling: 1 });
|
frm.set_query("default_price_list", { selling: 1 });
|
||||||
frm.set_query("account", "accounts", function (doc, cdt, cdn) {
|
frm.set_query("account", "accounts", function (doc, cdt, cdn) {
|
||||||
let d = locals[cdt][cdn];
|
let d = locals[cdt][cdn];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def get_data():
|
|||||||
"transactions": [
|
"transactions": [
|
||||||
{"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]},
|
{"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]},
|
||||||
{"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]},
|
{"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]},
|
||||||
{"label": _("Payments"), "items": ["Payment Entry", "Bank Account"]},
|
{"label": _("Payments"), "items": ["Payment Entry", "Bank Account", "Dunning"]},
|
||||||
{
|
{
|
||||||
"label": _("Support"),
|
"label": _("Support"),
|
||||||
"items": ["Issue", "Maintenance Visit", "Installation Note", "Warranty Claim"],
|
"items": ["Issue", "Maintenance Visit", "Installation Note", "Warranty Claim"],
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ frappe.ui.form.on("Quotation", {
|
|||||||
erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController {
|
erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController {
|
||||||
onload(doc, dt, dn) {
|
onload(doc, dt, dn) {
|
||||||
super.onload(doc, dt, dn);
|
super.onload(doc, dt, dn);
|
||||||
|
|
||||||
|
this.frm.trigger("disable_customer_if_creating_from_opportunity");
|
||||||
}
|
}
|
||||||
party_name() {
|
party_name() {
|
||||||
var me = this;
|
var me = this;
|
||||||
@@ -373,6 +375,12 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disable_customer_if_creating_from_opportunity(doc) {
|
||||||
|
if (doc.opportunity) {
|
||||||
|
this.frm.set_df_property("party_name", "read_only", 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
cur_frm.script_manager.make(erpnext.selling.QuotationController);
|
cur_frm.script_manager.make(erpnext.selling.QuotationController);
|
||||||
|
|||||||
@@ -875,7 +875,7 @@ def make_material_request(source_name, target_doc=None):
|
|||||||
"field_map": {
|
"field_map": {
|
||||||
"name": "sales_order_item",
|
"name": "sales_order_item",
|
||||||
"parent": "sales_order",
|
"parent": "sales_order",
|
||||||
"delivery_date": "required_by",
|
"delivery_date": "schedule_date",
|
||||||
"bom_no": "bom_no",
|
"bom_no": "bom_no",
|
||||||
},
|
},
|
||||||
"condition": lambda item: not frappe.db.exists(
|
"condition": lambda item: not frappe.db.exists(
|
||||||
|
|||||||
@@ -23,10 +23,18 @@ frappe.listview_settings["Sales Order"] = {
|
|||||||
} else if (!doc.skip_delivery_note && flt(doc.per_delivered) < 100) {
|
} else if (!doc.skip_delivery_note && flt(doc.per_delivered) < 100) {
|
||||||
if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
|
if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
|
||||||
// not delivered & overdue
|
// not delivered & overdue
|
||||||
return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"];
|
return [
|
||||||
|
__("Overdue"),
|
||||||
|
"red",
|
||||||
|
"per_delivered,<,100|delivery_date,<,Today|status,!=,Closed|docstatus,=,1",
|
||||||
|
];
|
||||||
} else if (flt(doc.grand_total) === 0) {
|
} else if (flt(doc.grand_total) === 0) {
|
||||||
// not delivered (zeroount order)
|
// not delivered (zeroount order)
|
||||||
return [__("To Deliver"), "orange", "per_delivered,<,100|grand_total,=,0|status,!=,Closed"];
|
return [
|
||||||
|
__("To Deliver"),
|
||||||
|
"orange",
|
||||||
|
"per_delivered,<,100|grand_total,=,0|status,!=,Closed|docstatus,=,1",
|
||||||
|
];
|
||||||
} else if (flt(doc.per_billed) < 100) {
|
} else if (flt(doc.per_billed) < 100) {
|
||||||
// not delivered & not billed
|
// not delivered & not billed
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -149,6 +149,26 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
this.make_app();
|
this.make_app();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frappe.realtime.on(`poe_${this.pos_opening}_closed`, (data) => {
|
||||||
|
const route = frappe.get_route_str();
|
||||||
|
if (data && route == "point-of-sale") {
|
||||||
|
frappe.dom.freeze();
|
||||||
|
frappe.msgprint({
|
||||||
|
title: __("POS Closed"),
|
||||||
|
indicator: "orange",
|
||||||
|
message: __("POS has been closed at {0}. Please refresh the page.", [
|
||||||
|
frappe.datetime.str_to_user(data.creation).bold(),
|
||||||
|
]),
|
||||||
|
primary_action_label: __("Refresh"),
|
||||||
|
primary_action: {
|
||||||
|
action() {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
set_opening_entry_status() {
|
set_opening_entry_status() {
|
||||||
|
|||||||
@@ -639,7 +639,7 @@
|
|||||||
"doc_view": "List",
|
"doc_view": "List",
|
||||||
"label": "Learn Sales Management",
|
"label": "Learn Sales Management",
|
||||||
"type": "URL",
|
"type": "URL",
|
||||||
"url": "https://frappe.school/courses/sales-management-course?utm_source=in_app"
|
"url": "https://school.frappe.io/lms/courses/sales-management-course?utm_source=in_app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Point of Sale",
|
"label": "Point of Sale",
|
||||||
@@ -676,5 +676,6 @@
|
|||||||
"type": "Dashboard"
|
"type": "Dashboard"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Selling"
|
"title": "Selling",
|
||||||
|
"type": "Workspace"
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,14 @@ from frappe.model.document import Document
|
|||||||
from frappe.utils import cint, comma_and, create_batch, get_link_to_form
|
from frappe.utils import cint, comma_and, create_batch, get_link_to_form
|
||||||
from frappe.utils.background_jobs import get_job, is_job_enqueued
|
from frappe.utils.background_jobs import get_job, is_job_enqueued
|
||||||
|
|
||||||
|
LEDGER_ENTRY_DOCTYPES = frozenset(
|
||||||
|
(
|
||||||
|
"GL Entry",
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TransactionDeletionRecord(Document):
|
class TransactionDeletionRecord(Document):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
@@ -475,31 +483,31 @@ def get_doctypes_to_be_ignored():
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def is_deletion_doc_running(company: str | None = None, err_msg: str | None = None):
|
def is_deletion_doc_running(company: str | None = None, err_msg: str | None = None):
|
||||||
if company:
|
if not company:
|
||||||
if running_deletion_jobs := frappe.db.get_all(
|
return
|
||||||
"Transaction Deletion Record",
|
|
||||||
filters={"docstatus": 1, "company": company, "status": "Running"},
|
running_deletion_job = frappe.db.get_value(
|
||||||
):
|
"Transaction Deletion Record",
|
||||||
if not err_msg:
|
{"docstatus": 1, "company": company, "status": "Running"},
|
||||||
err_msg = ""
|
"name",
|
||||||
frappe.throw(
|
)
|
||||||
title=_("Deletion in Progress!"),
|
|
||||||
msg=_("Transaction Deletion Document: {0} is running for this Company. {1}").format(
|
if not running_deletion_job:
|
||||||
get_link_to_form("Transaction Deletion Record", running_deletion_jobs[0].name), err_msg
|
return
|
||||||
),
|
|
||||||
)
|
frappe.throw(
|
||||||
|
title=_("Deletion in Progress!"),
|
||||||
|
msg=_("Transaction Deletion Document: {0} is running for this Company. {1}").format(
|
||||||
|
get_link_to_form("Transaction Deletion Record", running_deletion_job), err_msg or ""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_for_running_deletion_job(doc, method=None):
|
def check_for_running_deletion_job(doc, method=None):
|
||||||
# Check if DocType has 'company' field
|
# Check if DocType has 'company' field
|
||||||
if doc.doctype not in ("GL Entry", "Payment Ledger Entry", "Stock Ledger Entry"):
|
if doc.doctype in LEDGER_ENTRY_DOCTYPES or not doc.meta.has_field("company"):
|
||||||
df = qb.DocType("DocField")
|
return
|
||||||
if (
|
|
||||||
qb.from_(df)
|
is_deletion_doc_running(
|
||||||
.select(df.parent)
|
doc.company, _("Cannot make any transactions until the deletion job is completed")
|
||||||
.where((df.fieldname == "company") & (df.parent == doc.doctype))
|
)
|
||||||
.run()
|
|
||||||
):
|
|
||||||
is_deletion_doc_running(
|
|
||||||
doc.company, _("Cannot make any transactions until the deletion job is completed")
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ def add_standard_navbar_items():
|
|||||||
{
|
{
|
||||||
"item_label": "Frappe School",
|
"item_label": "Frappe School",
|
||||||
"item_type": "Route",
|
"item_type": "Route",
|
||||||
"route": "https://frappe.school?utm_source=in_app",
|
"route": "https://frappe.io/school?utm_source=in_app",
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ frappe.listview_settings["Delivery Note"] = {
|
|||||||
} else if (doc.status === "Return Issued") {
|
} else if (doc.status === "Return Issued") {
|
||||||
return [__("Return Issued"), "grey", "status,=,Return Issued"];
|
return [__("Return Issued"), "grey", "status,=,Return Issued"];
|
||||||
} else if (flt(doc.per_billed, 2) < 100) {
|
} else if (flt(doc.per_billed, 2) < 100) {
|
||||||
return [__("To Bill"), "orange", "per_billed,<,100"];
|
return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"];
|
||||||
} else if (flt(doc.per_billed, 2) === 100) {
|
} else if (flt(doc.per_billed, 2) === 100) {
|
||||||
return [__("Completed"), "green", "per_billed,=,100"];
|
return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onload: function (doclist) {
|
onload: function (doclist) {
|
||||||
|
|||||||
@@ -1196,7 +1196,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def get_purchase_voucher_details(doctype, item_code, document_name):
|
def get_purchase_voucher_details(doctype, item_code, document_name=None):
|
||||||
parent_doc = frappe.qb.DocType(doctype)
|
parent_doc = frappe.qb.DocType(doctype)
|
||||||
child_doc = frappe.qb.DocType(doctype + " Item")
|
child_doc = frappe.qb.DocType(doctype + " Item")
|
||||||
|
|
||||||
@@ -1215,9 +1215,11 @@ def get_purchase_voucher_details(doctype, item_code, document_name):
|
|||||||
)
|
)
|
||||||
.where(parent_doc.docstatus == 1)
|
.where(parent_doc.docstatus == 1)
|
||||||
.where(child_doc.item_code == item_code)
|
.where(child_doc.item_code == item_code)
|
||||||
.where(parent_doc.name != document_name)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if document_name:
|
||||||
|
query = query.where(parent_doc.name != document_name)
|
||||||
|
|
||||||
if doctype in ("Purchase Receipt", "Purchase Invoice"):
|
if doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||||
query = query.select(parent_doc.posting_date, parent_doc.posting_time)
|
query = query.select(parent_doc.posting_date, parent_doc.posting_time)
|
||||||
query = query.orderby(
|
query = query.orderby(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ frappe.listview_settings["Material Request"] = {
|
|||||||
return [__("Completed"), "green"];
|
return [__("Completed"), "green"];
|
||||||
}
|
}
|
||||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
|
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
|
||||||
return [__("Pending"), "orange", "per_ordered,=,0"];
|
return [__("Pending"), "orange", "per_ordered,=,0|docstatus,=,1"];
|
||||||
} else if (
|
} else if (
|
||||||
doc.docstatus == 1 &&
|
doc.docstatus == 1 &&
|
||||||
flt(doc.per_ordered, precision) < 100 &&
|
flt(doc.per_ordered, precision) < 100 &&
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ frappe.listview_settings["Purchase Receipt"] = {
|
|||||||
} else if (doc.status === "Closed") {
|
} else if (doc.status === "Closed") {
|
||||||
return [__("Closed"), "green", "status,=,Closed"];
|
return [__("Closed"), "green", "status,=,Closed"];
|
||||||
} else if (flt(doc.per_returned, 2) === 100) {
|
} else if (flt(doc.per_returned, 2) === 100) {
|
||||||
return [__("Return Issued"), "grey", "per_returned,=,100"];
|
return [__("Return Issued"), "grey", "per_returned,=,100|docstatus,=,1"];
|
||||||
} else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) == 0) {
|
} else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) == 0) {
|
||||||
return [__("To Bill"), "orange", "per_billed,<,100"];
|
return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"];
|
||||||
} else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) {
|
} else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) {
|
||||||
return [__("Partly Billed"), "yellow", "per_billed,<,100"];
|
return [__("Partly Billed"), "yellow", "per_billed,<,100|docstatus,=,1"];
|
||||||
} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
|
} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
|
||||||
return [__("Completed"), "green", "per_billed,=,100"];
|
return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -4077,6 +4077,54 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
pr.reload()
|
pr.reload()
|
||||||
self.assertEqual(pr.status, "To Bill")
|
self.assertEqual(pr.status, "To Bill")
|
||||||
|
|
||||||
|
def test_recreate_stock_ledgers(self):
|
||||||
|
item_code = "Test Item for Recreate Stock Ledgers"
|
||||||
|
create_item(item_code)
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(item_code=item_code, qty=10, rate=100)
|
||||||
|
pr.submit()
|
||||||
|
|
||||||
|
sles = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
filters={"voucher_type": pr.doctype, "voucher_no": pr.name},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(sles)
|
||||||
|
|
||||||
|
for row in sles:
|
||||||
|
doc = frappe.get_doc("Stock Ledger Entry", row)
|
||||||
|
doc.delete()
|
||||||
|
|
||||||
|
sles = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
filters={"voucher_type": pr.doctype, "voucher_no": pr.name},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(sles)
|
||||||
|
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Repost Item Valuation",
|
||||||
|
"based_on": "Transaction",
|
||||||
|
"voucher_type": pr.doctype,
|
||||||
|
"voucher_no": pr.name,
|
||||||
|
"posting_date": pr.posting_date,
|
||||||
|
"posting_time": pr.posting_time,
|
||||||
|
"company": pr.company,
|
||||||
|
"recreate_stock_ledgers": 1,
|
||||||
|
}
|
||||||
|
).submit()
|
||||||
|
|
||||||
|
sles = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
filters={"voucher_type": pr.doctype, "voucher_no": pr.name},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(sles)
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"allow_negative_stock",
|
"allow_negative_stock",
|
||||||
"via_landed_cost_voucher",
|
"via_landed_cost_voucher",
|
||||||
"allow_zero_rate",
|
"allow_zero_rate",
|
||||||
|
"recreate_stock_ledgers",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"error_section",
|
"error_section",
|
||||||
"error_log",
|
"error_log",
|
||||||
@@ -220,12 +221,20 @@
|
|||||||
"label": "Reposting Data File",
|
"label": "Reposting Data File",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.based_on == \"Transaction\"",
|
||||||
|
"fieldname": "recreate_stock_ledgers",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Recreate Stock Ledgers"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-06-27 16:55:23.150146",
|
"modified": "2025-03-31 12:38:20.566196",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Repost Item Valuation",
|
"name": "Repost Item Valuation",
|
||||||
@@ -274,7 +283,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class RepostItemValuation(Document):
|
|||||||
items_to_be_repost: DF.Code | None
|
items_to_be_repost: DF.Code | None
|
||||||
posting_date: DF.Date
|
posting_date: DF.Date
|
||||||
posting_time: DF.Time | None
|
posting_time: DF.Time | None
|
||||||
|
recreate_stock_ledgers: DF.Check
|
||||||
reposting_data_file: DF.Attach | None
|
reposting_data_file: DF.Attach | None
|
||||||
status: DF.Literal["Queued", "In Progress", "Completed", "Skipped", "Failed"]
|
status: DF.Literal["Queued", "In Progress", "Completed", "Skipped", "Failed"]
|
||||||
total_reposting_count: DF.Int
|
total_reposting_count: DF.Int
|
||||||
@@ -74,6 +75,7 @@ class RepostItemValuation(Document):
|
|||||||
self.reset_field_values()
|
self.reset_field_values()
|
||||||
self.set_company()
|
self.set_company()
|
||||||
self.validate_accounts_freeze()
|
self.validate_accounts_freeze()
|
||||||
|
self.reset_recreate_stock_ledgers()
|
||||||
|
|
||||||
def validate_period_closing_voucher(self):
|
def validate_period_closing_voucher(self):
|
||||||
# Period Closing Voucher
|
# Period Closing Voucher
|
||||||
@@ -105,6 +107,10 @@ class RepostItemValuation(Document):
|
|||||||
msg = f"Due to closing stock balance {name}, you cannot repost item valuation before {to_date}"
|
msg = f"Due to closing stock balance {name}, you cannot repost item valuation before {to_date}"
|
||||||
frappe.throw(_(msg))
|
frappe.throw(_(msg))
|
||||||
|
|
||||||
|
def reset_recreate_stock_ledgers(self):
|
||||||
|
if self.recreate_stock_ledgers and self.based_on != "Transaction":
|
||||||
|
self.recreate_stock_ledgers = 0
|
||||||
|
|
||||||
def get_closing_stock_balance(self):
|
def get_closing_stock_balance(self):
|
||||||
filters = {
|
filters = {
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
@@ -250,6 +256,16 @@ class RepostItemValuation(Document):
|
|||||||
filters,
|
filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def recreate_stock_ledger_entries(self):
|
||||||
|
"""Recreate Stock Ledger Entries for the transaction."""
|
||||||
|
if self.based_on == "Transaction" and self.recreate_stock_ledgers:
|
||||||
|
doc = frappe.get_doc(self.voucher_type, self.voucher_no)
|
||||||
|
doc.docstatus = 2
|
||||||
|
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
|
||||||
|
|
||||||
|
doc.docstatus = 1
|
||||||
|
doc.update_stock_ledger(allow_negative_stock=True)
|
||||||
|
|
||||||
|
|
||||||
def on_doctype_update():
|
def on_doctype_update():
|
||||||
frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse")
|
frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse")
|
||||||
@@ -268,6 +284,9 @@ def repost(doc):
|
|||||||
if not frappe.flags.in_test:
|
if not frappe.flags.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
if doc.recreate_stock_ledgers:
|
||||||
|
doc.recreate_stock_ledger_entries()
|
||||||
|
|
||||||
repost_sl_entries(doc)
|
repost_sl_entries(doc)
|
||||||
repost_gl_entries(doc)
|
repost_gl_entries(doc)
|
||||||
|
|
||||||
@@ -291,7 +310,7 @@ def repost(doc):
|
|||||||
|
|
||||||
status = "Failed"
|
status = "Failed"
|
||||||
# If failed because of timeout, set status to In Progress
|
# If failed because of timeout, set status to In Progress
|
||||||
if traceback and "timeout" in traceback.lower():
|
if traceback and ("timeout" in traceback.lower() or "Deadlock found" in traceback):
|
||||||
status = "In Progress"
|
status = "In Progress"
|
||||||
|
|
||||||
if traceback:
|
if traceback:
|
||||||
@@ -306,13 +325,14 @@ def repost(doc):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
outgoing_email_account = frappe.get_cached_value(
|
if status == "Failed":
|
||||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
outgoing_email_account = frappe.get_cached_value(
|
||||||
)
|
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||||
|
)
|
||||||
|
|
||||||
if outgoing_email_account and not isinstance(e, RecoverableErrors):
|
if outgoing_email_account and not isinstance(e, RecoverableErrors):
|
||||||
notify_error_to_stock_managers(doc, message)
|
notify_error_to_stock_managers(doc, message)
|
||||||
doc.set_status("Failed")
|
doc.set_status("Failed")
|
||||||
finally:
|
finally:
|
||||||
if not frappe.flags.in_test:
|
if not frappe.flags.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|||||||
@@ -1907,6 +1907,59 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
self.assertEqual(sle.stock_value_difference, 100)
|
self.assertEqual(sle.stock_value_difference, 100)
|
||||||
self.assertEqual(sle.stock_value, 100 * i)
|
self.assertEqual(sle.stock_value, 100 * i)
|
||||||
|
|
||||||
|
def test_stock_entry_amount(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
rm_item_code = "Test Stock Entry Amount 1"
|
||||||
|
make_item(rm_item_code, {"is_stock_item": 1})
|
||||||
|
|
||||||
|
fg_item_code = "Test Repack Stock Entry Amount 1"
|
||||||
|
make_item(fg_item_code, {"is_stock_item": 1})
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=rm_item_code,
|
||||||
|
qty=1,
|
||||||
|
to_warehouse=warehouse,
|
||||||
|
basic_rate=200,
|
||||||
|
posting_date=nowdate(),
|
||||||
|
)
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=rm_item_code,
|
||||||
|
qty=1,
|
||||||
|
purpose="Repack",
|
||||||
|
basic_rate=100,
|
||||||
|
do_not_save=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
se.items[0].s_warehouse = warehouse
|
||||||
|
se.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": fg_item_code,
|
||||||
|
"qty": 1,
|
||||||
|
"t_warehouse": warehouse,
|
||||||
|
"uom": "Nos",
|
||||||
|
"conversion_factor": 1.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
se.set_stock_entry_type()
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
self.assertEqual(se.items[0].amount, 200)
|
||||||
|
self.assertEqual(se.items[0].basic_amount, 200)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=rm_item_code,
|
||||||
|
qty=1,
|
||||||
|
to_warehouse=warehouse,
|
||||||
|
basic_rate=300,
|
||||||
|
posting_date=add_days(nowdate(), -1),
|
||||||
|
)
|
||||||
|
|
||||||
|
se.reload()
|
||||||
|
self.assertEqual(se.items[0].amount, 300)
|
||||||
|
self.assertEqual(se.items[0].basic_amount, 300)
|
||||||
|
|
||||||
|
|
||||||
def make_serialized_item(**args):
|
def make_serialized_item(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
"allow_to_edit_stock_uom_qty_for_sales",
|
"allow_to_edit_stock_uom_qty_for_sales",
|
||||||
"column_break_lznj",
|
"column_break_lznj",
|
||||||
"allow_to_edit_stock_uom_qty_for_purchase",
|
"allow_to_edit_stock_uom_qty_for_purchase",
|
||||||
|
"section_break_ylhd",
|
||||||
|
"allow_uom_with_conversion_rate_defined_in_item",
|
||||||
"stock_validations_tab",
|
"stock_validations_tab",
|
||||||
"section_break_9",
|
"section_break_9",
|
||||||
"over_delivery_receipt_allowance",
|
"over_delivery_receipt_allowance",
|
||||||
@@ -490,6 +492,17 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_wslv",
|
"fieldname": "column_break_wslv",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ylhd",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "If enabled, the system will allow selecting UOMs in sales and purchase transactions only if the conversion rate is set in the item master.",
|
||||||
|
"fieldname": "allow_uom_with_conversion_rate_defined_in_item",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow UOM with Conversion Rate Defined in Item"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -497,7 +510,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-02-28 16:08:35.938840",
|
"modified": "2025-03-31 15:34:20.752065",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
@@ -518,7 +531,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class StockSettings(Document):
|
|||||||
allow_partial_reservation: DF.Check
|
allow_partial_reservation: DF.Check
|
||||||
allow_to_edit_stock_uom_qty_for_purchase: DF.Check
|
allow_to_edit_stock_uom_qty_for_purchase: DF.Check
|
||||||
allow_to_edit_stock_uom_qty_for_sales: DF.Check
|
allow_to_edit_stock_uom_qty_for_sales: DF.Check
|
||||||
|
allow_uom_with_conversion_rate_defined_in_item: DF.Check
|
||||||
auto_create_serial_and_batch_bundle_for_outward: DF.Check
|
auto_create_serial_and_batch_bundle_for_outward: DF.Check
|
||||||
auto_indent: DF.Check
|
auto_indent: DF.Check
|
||||||
auto_insert_price_list_rate_if_missing: DF.Check
|
auto_insert_price_list_rate_if_missing: DF.Check
|
||||||
|
|||||||
@@ -406,16 +406,17 @@ class StockBalanceReport:
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for dimension in get_inventory_dimensions():
|
if self.filters.get("show_dimension_wise_stock"):
|
||||||
columns.append(
|
for dimension in get_inventory_dimensions():
|
||||||
{
|
columns.append(
|
||||||
"label": _(dimension.doctype),
|
{
|
||||||
"fieldname": dimension.fieldname,
|
"label": _(dimension.doctype),
|
||||||
"fieldtype": "Link",
|
"fieldname": dimension.fieldname,
|
||||||
"options": dimension.doctype,
|
"fieldtype": "Link",
|
||||||
"width": 110,
|
"options": dimension.doctype,
|
||||||
}
|
"width": 110,
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
columns.extend(
|
columns.extend(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -184,11 +184,16 @@ def validate_serial_no(sle):
|
|||||||
frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
|
frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
|
||||||
|
|
||||||
|
|
||||||
def validate_cancellation(args):
|
def validate_cancellation(kargs):
|
||||||
if args[0].get("is_cancelled"):
|
if kargs[0].get("is_cancelled"):
|
||||||
repost_entry = frappe.db.get_value(
|
repost_entry = frappe.db.get_value(
|
||||||
"Repost Item Valuation",
|
"Repost Item Valuation",
|
||||||
{"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
|
{
|
||||||
|
"voucher_type": kargs[0].voucher_type,
|
||||||
|
"voucher_no": kargs[0].voucher_no,
|
||||||
|
"docstatus": 1,
|
||||||
|
"recreate_stock_ledgers": 0,
|
||||||
|
},
|
||||||
["name", "status"],
|
["name", "status"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
@@ -1233,7 +1238,11 @@ class update_entries_after:
|
|||||||
stock_entry.db_update()
|
stock_entry.db_update()
|
||||||
for d in stock_entry.items:
|
for d in stock_entry.items:
|
||||||
# Update only the row that matches the voucher_detail_no or the row containing the FG/Scrap Item.
|
# Update only the row that matches the voucher_detail_no or the row containing the FG/Scrap Item.
|
||||||
if d.name == voucher_detail_no or (not d.s_warehouse and d.t_warehouse):
|
if (
|
||||||
|
d.name == voucher_detail_no
|
||||||
|
or (not d.s_warehouse and d.t_warehouse)
|
||||||
|
or stock_entry.purpose in ["Manufacture", "Repack"]
|
||||||
|
):
|
||||||
d.db_update()
|
d.db_update()
|
||||||
|
|
||||||
def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
|
def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
|
||||||
|
|||||||
@@ -802,7 +802,7 @@
|
|||||||
"doc_view": "List",
|
"doc_view": "List",
|
||||||
"label": "Learn Inventory Management",
|
"label": "Learn Inventory Management",
|
||||||
"type": "URL",
|
"type": "URL",
|
||||||
"url": "https://frappe.school/courses/inventory-management?utm_source=in_app"
|
"url": "https://school.frappe.io/lms/courses/inventory-management?utm_source=in_app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"color": "Yellow",
|
"color": "Yellow",
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ def get_repeated(values):
|
|||||||
|
|
||||||
|
|
||||||
def get_documents_with_active_service_level_agreement():
|
def get_documents_with_active_service_level_agreement():
|
||||||
sla_doctypes = frappe.cache().hget("service_level_agreement", "active")
|
sla_doctypes = frappe.cache.get_value("doctypes_with_active_sla")
|
||||||
|
|
||||||
if sla_doctypes is None:
|
if sla_doctypes is None:
|
||||||
return set_documents_with_active_service_level_agreement()
|
return set_documents_with_active_service_level_agreement()
|
||||||
@@ -484,20 +484,22 @@ def get_documents_with_active_service_level_agreement():
|
|||||||
|
|
||||||
|
|
||||||
def set_documents_with_active_service_level_agreement():
|
def set_documents_with_active_service_level_agreement():
|
||||||
active = [
|
active = frozenset(
|
||||||
sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])
|
sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])
|
||||||
]
|
)
|
||||||
frappe.cache().hset("service_level_agreement", "active", active)
|
frappe.cache.set_value("doctypes_with_active_sla", active)
|
||||||
return active
|
return active
|
||||||
|
|
||||||
|
|
||||||
def apply(doc, method=None):
|
def apply(doc, method=None):
|
||||||
# Applies SLA to document on validate
|
# Applies SLA to document on validate
|
||||||
|
flags = frappe.local.flags
|
||||||
|
|
||||||
if (
|
if (
|
||||||
frappe.flags.in_patch
|
flags.in_patch
|
||||||
or frappe.flags.in_migrate
|
or flags.in_migrate
|
||||||
or frappe.flags.in_install
|
or flags.in_install
|
||||||
or frappe.flags.in_setup_wizard
|
or flags.in_setup_wizard
|
||||||
or doc.doctype not in get_documents_with_active_service_level_agreement()
|
or doc.doctype not in get_documents_with_active_service_level_agreement()
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
|
<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
|
||||||
class="btn btn-primary btn-sm" id="pay-for-order">
|
class="btn btn-primary btn-sm" id="pay-for-order">
|
||||||
{{ _("Pay", null, "Amount") }} {{doc.get_formatted("grand_total") }}
|
{{ _("Pay", null, "Amount") }} {{ pay_amount }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from frappe import _
|
|||||||
|
|
||||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||||
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST,
|
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST,
|
||||||
|
get_amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -50,11 +51,7 @@ def get_context(context):
|
|||||||
)
|
)
|
||||||
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
|
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
|
||||||
|
|
||||||
context.show_pay_button = (
|
context.show_pay_button, context.pay_amount = get_payment_details(context.doc)
|
||||||
"payments" in frappe.get_installed_apps()
|
|
||||||
and frappe.db.get_single_value("Buying Settings", "show_pay_button")
|
|
||||||
and context.doc.doctype in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST
|
|
||||||
)
|
|
||||||
context.show_make_pi_button = False
|
context.show_make_pi_button = False
|
||||||
if context.doc.get("supplier"):
|
if context.doc.get("supplier"):
|
||||||
# show Make Purchase Invoice button based on permission
|
# show Make Purchase Invoice button based on permission
|
||||||
@@ -67,3 +64,19 @@ def get_attachments(dt, dn):
|
|||||||
fields=["name", "file_name", "file_url", "is_private"],
|
fields=["name", "file_name", "file_url", "is_private"],
|
||||||
filters={"attached_to_name": dn, "attached_to_doctype": dt, "is_private": 0},
|
filters={"attached_to_name": dn, "attached_to_doctype": dt, "is_private": 0},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_details(doc):
|
||||||
|
show_pay_button, amount = (
|
||||||
|
(
|
||||||
|
"payments" in frappe.get_installed_apps()
|
||||||
|
and frappe.db.get_single_value("Buying Settings", "show_pay_button")
|
||||||
|
and doc.doctype in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if not show_pay_button:
|
||||||
|
return show_pay_button, amount
|
||||||
|
|
||||||
|
amount = get_amount(doc)
|
||||||
|
return bool(amount), amount
|
||||||
|
|||||||
@@ -508,9 +508,9 @@ Close Loan,Darlehen schließen,
|
|||||||
Close the POS,Schließen Sie die Kasse,
|
Close the POS,Schließen Sie die Kasse,
|
||||||
Closed,Geschlossen,
|
Closed,Geschlossen,
|
||||||
Closed order cannot be cancelled. Unclose to cancel.,Geschlosser Auftrag kann nicht abgebrochen werden. Bitte wiedereröffnen um abzubrechen.,
|
Closed order cannot be cancelled. Unclose to cancel.,Geschlosser Auftrag kann nicht abgebrochen werden. Bitte wiedereröffnen um abzubrechen.,
|
||||||
Closing (Cr),Schlußstand (Haben),
|
Closing (Cr),Schlußstand (H),
|
||||||
Closing (Dr),Schlußstand (Soll),
|
Closing (Dr),Schlußstand (S),
|
||||||
Closing (Opening + Total),Schließen (Eröffnung + Gesamt),
|
Closing (Opening + Total),Schlußstand (Anfangssstand + Summe),
|
||||||
Closing Account {0} must be of type Liability / Equity,Abschlußkonto {0} muss vom Typ Verbindlichkeiten/Eigenkapital sein,
|
Closing Account {0} must be of type Liability / Equity,Abschlußkonto {0} muss vom Typ Verbindlichkeiten/Eigenkapital sein,
|
||||||
Closing Balance,Schlussbilanz,
|
Closing Balance,Schlussbilanz,
|
||||||
Code,Code,
|
Code,Code,
|
||||||
@@ -598,7 +598,7 @@ Course Code: ,Kurscode:,
|
|||||||
Course Enrollment {0} does not exists,Die Kursanmeldung {0} existiert nicht,
|
Course Enrollment {0} does not exists,Die Kursanmeldung {0} existiert nicht,
|
||||||
Course Schedule,Kurstermine,
|
Course Schedule,Kurstermine,
|
||||||
Course: ,Kurs:,
|
Course: ,Kurs:,
|
||||||
Cr,Haben,
|
Cr,H,
|
||||||
Create,Erstellen,
|
Create,Erstellen,
|
||||||
Create BOM,Stückliste anlegen,
|
Create BOM,Stückliste anlegen,
|
||||||
Create Delivery Trip,Erstelle Auslieferungsfahrt,
|
Create Delivery Trip,Erstelle Auslieferungsfahrt,
|
||||||
@@ -647,8 +647,8 @@ Creating Fees,Gebühren anlegen,
|
|||||||
Creating student groups,Erstelle Studentengruppen,
|
Creating student groups,Erstelle Studentengruppen,
|
||||||
Creating {0} Invoice,{0} Rechnung erstellen,
|
Creating {0} Invoice,{0} Rechnung erstellen,
|
||||||
Credit,Haben,
|
Credit,Haben,
|
||||||
Credit ({0}),Guthaben ({0}),
|
Credit ({0}),Haben ({0}),
|
||||||
Credit Account,Guthabenkonto,
|
Credit Account,Haben-Konto,
|
||||||
Credit Balance,Verfügbarer Kredit,
|
Credit Balance,Verfügbarer Kredit,
|
||||||
Credit Card,Kreditkarte,
|
Credit Card,Kreditkarte,
|
||||||
Credit Days cannot be a negative number,Kredit-Tage können keine negative Zahl sein,
|
Credit Days cannot be a negative number,Kredit-Tage können keine negative Zahl sein,
|
||||||
@@ -712,14 +712,14 @@ Date of Transaction,Datum der Transaktion,
|
|||||||
Day,Tag,
|
Day,Tag,
|
||||||
Debit,Soll,
|
Debit,Soll,
|
||||||
Debit ({0}),Soll ({0}),
|
Debit ({0}),Soll ({0}),
|
||||||
Debit Account,Sollkonto,
|
Debit Account,Soll-Konto,
|
||||||
Debit Note,Lastschrift,
|
Debit Note,Lastschrift,
|
||||||
Debit Note Amount,Lastschriftbetrag,
|
Debit Note Amount,Lastschriftbetrag,
|
||||||
Debit Note Issued,Lastschrift ausgestellt am,
|
Debit Note Issued,Lastschrift ausgestellt am,
|
||||||
Debit To is required,Debit Um erforderlich,
|
Debit To is required,Forderungskonto ist erforderlich,
|
||||||
Debit and Credit not equal for {0} #{1}. Difference is {2}.,Soll und Haben nicht gleich für {0} #{1}. Unterschied ist {2}.,
|
Debit and Credit not equal for {0} #{1}. Difference is {2}.,Soll und Haben nicht gleich für {0} #{1}. Unterschied ist {2}.,
|
||||||
Debtors,Schuldner,
|
Debtors,Debitoren,
|
||||||
Debtors ({0}),Schuldnern ({0}),
|
Debtors ({0}),Debitoren ({0}),
|
||||||
Declare Lost,Für verloren erklären,
|
Declare Lost,Für verloren erklären,
|
||||||
Default Activity Cost exists for Activity Type - {0},Es gibt Standard-Aktivitätskosten für Aktivitätsart - {0},
|
Default Activity Cost exists for Activity Type - {0},Es gibt Standard-Aktivitätskosten für Aktivitätsart - {0},
|
||||||
Default BOM ({0}) must be active for this item or its template,Standardstückliste ({0}) muss für diesen Artikel oder dessen Vorlage aktiv sein,
|
Default BOM ({0}) must be active for this item or its template,Standardstückliste ({0}) muss für diesen Artikel oder dessen Vorlage aktiv sein,
|
||||||
@@ -1628,7 +1628,7 @@ Open Item {0},Offene-Posten {0},
|
|||||||
Open Notifications,Offene Benachrichtigungen,
|
Open Notifications,Offene Benachrichtigungen,
|
||||||
Open Orders,Offene Bestellungen,
|
Open Orders,Offene Bestellungen,
|
||||||
Open a new ticket,Öffnen Sie ein neues Ticket,
|
Open a new ticket,Öffnen Sie ein neues Ticket,
|
||||||
Opening,Eröffnung,
|
Opening,Anfangssstand,
|
||||||
Opening (Cr),Anfangssstand (Haben),
|
Opening (Cr),Anfangssstand (Haben),
|
||||||
Opening (Dr),Anfangsstand (Soll),
|
Opening (Dr),Anfangsstand (Soll),
|
||||||
Opening Accounting Balance,Eröffnungsbilanz,
|
Opening Accounting Balance,Eröffnungsbilanz,
|
||||||
@@ -2215,8 +2215,8 @@ Retained Earnings,Gewinnrücklagen,
|
|||||||
Retention Stock Entry,Vorratsbestandseintrag,
|
Retention Stock Entry,Vorratsbestandseintrag,
|
||||||
Retention Stock Entry already created or Sample Quantity not provided,Aufbewahrungsbestandseintrag bereits angelegt oder Musterbestand nicht bereitgestellt,
|
Retention Stock Entry already created or Sample Quantity not provided,Aufbewahrungsbestandseintrag bereits angelegt oder Musterbestand nicht bereitgestellt,
|
||||||
Return,Zurück,
|
Return,Zurück,
|
||||||
Return / Credit Note,Return / Gutschrift,
|
Return / Credit Note,Rückgabe / Gutschrift,
|
||||||
Return / Debit Note,Return / Lastschrift,
|
Return / Debit Note,Rückgabe / Lastschrift,
|
||||||
Returns,Retouren,
|
Returns,Retouren,
|
||||||
Reverse Journal Entry,Buchungssatz umkehren,
|
Reverse Journal Entry,Buchungssatz umkehren,
|
||||||
Review Invitation Sent,Einladung überprüfen gesendet,
|
Review Invitation Sent,Einladung überprüfen gesendet,
|
||||||
@@ -2826,7 +2826,7 @@ To {0} | {1} {2},An {0} | {1} {2},
|
|||||||
Toggle Filters,Filter umschalten,
|
Toggle Filters,Filter umschalten,
|
||||||
Too many columns. Export the report and print it using a spreadsheet application.,Zu viele Spalten. Exportieren Sie den Bericht und drucken Sie ihn mit einem Tabellenkalkulationsprogramm aus.,
|
Too many columns. Export the report and print it using a spreadsheet application.,Zu viele Spalten. Exportieren Sie den Bericht und drucken Sie ihn mit einem Tabellenkalkulationsprogramm aus.,
|
||||||
Tools,Werkzeuge,
|
Tools,Werkzeuge,
|
||||||
Total (Credit),Insgesamt (Credit),
|
Total (Credit),Insgesamt (Haben),
|
||||||
Total (Without Tax),Summe (ohne Steuern),
|
Total (Without Tax),Summe (ohne Steuern),
|
||||||
Total Achieved,Gesamtsumme erreicht,
|
Total Achieved,Gesamtsumme erreicht,
|
||||||
Total Actual,Summe Tatsächlich,
|
Total Actual,Summe Tatsächlich,
|
||||||
@@ -2837,7 +2837,7 @@ Total Budget,Gesamtbudget; Gesamtetat,
|
|||||||
Total Collected: {0},Gesammelt gesammelt: {0},
|
Total Collected: {0},Gesammelt gesammelt: {0},
|
||||||
Total Commission,Gesamtprovision,
|
Total Commission,Gesamtprovision,
|
||||||
Total Contribution Amount: {0},Gesamtbeitragsbetrag: {0},
|
Total Contribution Amount: {0},Gesamtbeitragsbetrag: {0},
|
||||||
Total Credit/ Debit Amount should be same as linked Journal Entry,Der Gesamtkreditbetrag sollte identisch mit dem verknüpften Buchungssatz sein,
|
Total Credit/ Debit Amount should be same as linked Journal Entry,Der gesamte Soll-/ Habenbetrag sollte identisch mit dem verknüpften Buchungssatz sein,
|
||||||
Total Debit must be equal to Total Credit. The difference is {0},Gesamt-Soll muss gleich Gesamt-Haben sein. Die Differenz ist {0},
|
Total Debit must be equal to Total Credit. The difference is {0},Gesamt-Soll muss gleich Gesamt-Haben sein. Die Differenz ist {0},
|
||||||
Total Invoiced Amount,Gesamtrechnungsbetrag,
|
Total Invoiced Amount,Gesamtrechnungsbetrag,
|
||||||
Total Order Considered,Geschätzte Summe der Bestellungen,
|
Total Order Considered,Geschätzte Summe der Bestellungen,
|
||||||
@@ -2915,7 +2915,7 @@ Unable to find score starting at {0}. You need to have standing scores covering
|
|||||||
Unable to find variable: ,Variable kann nicht gefunden werden:,
|
Unable to find variable: ,Variable kann nicht gefunden werden:,
|
||||||
Unblock Invoice,Rechnung entsperren,
|
Unblock Invoice,Rechnung entsperren,
|
||||||
Uncheck all,Alle abwählen,
|
Uncheck all,Alle abwählen,
|
||||||
Unclosed Fiscal Years Profit / Loss (Credit),Offener Gewinn / Verlust (Kredit) des Geschäftsjahres,
|
Unclosed Fiscal Years Profit / Loss (Credit),Offener Gewinn / Verlust (Haben) des Geschäftsjahres,
|
||||||
Unit,Einheit,
|
Unit,Einheit,
|
||||||
Unit of Measure,Maßeinheit,
|
Unit of Measure,Maßeinheit,
|
||||||
Unit of Measure {0} has been entered more than once in Conversion Factor Table,Die Mengeneinheit {0} wurde mehr als einmal in die Umrechnungsfaktortabelle eingetragen.,
|
Unit of Measure {0} has been entered more than once in Conversion Factor Table,Die Mengeneinheit {0} wurde mehr als einmal in die Umrechnungsfaktortabelle eingetragen.,
|
||||||
@@ -3185,7 +3185,7 @@ on,Am,
|
|||||||
{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.,"{0} {1}: Kostenstelle ist erforderlich für ""Gewinn- und Verlust"" Konto {2}. Bitte erstellen Sie eine Standard-Kostenstelle für das Unternehmen.",
|
{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.,"{0} {1}: Kostenstelle ist erforderlich für ""Gewinn- und Verlust"" Konto {2}. Bitte erstellen Sie eine Standard-Kostenstelle für das Unternehmen.",
|
||||||
{0} {1}: Cost Center {2} does not belong to Company {3},{0} {1}: Kostenstelle {2} gehört nicht zu Unternehmen {3},
|
{0} {1}: Cost Center {2} does not belong to Company {3},{0} {1}: Kostenstelle {2} gehört nicht zu Unternehmen {3},
|
||||||
{0} {1}: Customer is required against Receivable account {2},{0} {1}: Für das Eingangskonto {2} ist ein Kunde erforderlich,
|
{0} {1}: Customer is required against Receivable account {2},{0} {1}: Für das Eingangskonto {2} ist ein Kunde erforderlich,
|
||||||
{0} {1}: Either debit or credit amount is required for {2},{0} {1}: Debit- oder Kreditbetrag ist für {2} erforderlich,
|
{0} {1}: Either debit or credit amount is required for {2},{0} {1}: Soll- oder Habenbetrag ist für {2} erforderlich,
|
||||||
{0} {1}: Supplier is required against Payable account {2},{0} {1}: Für das Kreditorenkonto ist ein Lieferant erforderlich {2},
|
{0} {1}: Supplier is required against Payable account {2},{0} {1}: Für das Kreditorenkonto ist ein Lieferant erforderlich {2},
|
||||||
{0}% Billed,{0}% berechnet,
|
{0}% Billed,{0}% berechnet,
|
||||||
{0}% Delivered,{0}% geliefert,
|
{0}% Delivered,{0}% geliefert,
|
||||||
@@ -3408,7 +3408,7 @@ Do you want to submit the material request,Möchten Sie die Materialanfrage einr
|
|||||||
Doctype,DocType,
|
Doctype,DocType,
|
||||||
Document {0} successfully uncleared,Dokument {0} wurde nicht erfolgreich gelöscht,
|
Document {0} successfully uncleared,Dokument {0} wurde nicht erfolgreich gelöscht,
|
||||||
Download Template,Vorlage herunterladen,
|
Download Template,Vorlage herunterladen,
|
||||||
Dr,Soll,
|
Dr,S,
|
||||||
Due Date,Fälligkeitsdatum,
|
Due Date,Fälligkeitsdatum,
|
||||||
Duplicate,Duplizieren,
|
Duplicate,Duplizieren,
|
||||||
Duplicate Project with Tasks,Projekt mit Aufgaben duplizieren,
|
Duplicate Project with Tasks,Projekt mit Aufgaben duplizieren,
|
||||||
@@ -3889,7 +3889,7 @@ Operation Id,Arbeitsgang-ID,
|
|||||||
Partially ordered,teilweise geordnete,
|
Partially ordered,teilweise geordnete,
|
||||||
Please select company first,Bitte wählen Sie zuerst die Firma aus,
|
Please select company first,Bitte wählen Sie zuerst die Firma aus,
|
||||||
Please select patient,Bitte wählen Sie Patient,
|
Please select patient,Bitte wählen Sie Patient,
|
||||||
Printed On ,Gedruckt auf,
|
Printed On ,Gedruckt am,
|
||||||
Projected qty,Geplante Menge,
|
Projected qty,Geplante Menge,
|
||||||
Sales person,Vertriebsmitarbeiter,
|
Sales person,Vertriebsmitarbeiter,
|
||||||
Serial No {0} Created,Seriennummer {0} Erstellt,
|
Serial No {0} Created,Seriennummer {0} Erstellt,
|
||||||
@@ -4338,9 +4338,9 @@ Auto Created,Automatisch erstellt,
|
|||||||
Stock User,Lager-Benutzer,
|
Stock User,Lager-Benutzer,
|
||||||
Fiscal Year Company,Geschäftsjahr Unternehmen,
|
Fiscal Year Company,Geschäftsjahr Unternehmen,
|
||||||
Debit Amount,Soll-Betrag,
|
Debit Amount,Soll-Betrag,
|
||||||
Credit Amount,Guthaben-Summe,
|
Credit Amount,Haben-Betrag,
|
||||||
Debit Amount in Account Currency,Soll-Betrag in Kontowährung,
|
Debit Amount in Account Currency,Soll-Betrag in Kontowährung,
|
||||||
Credit Amount in Account Currency,(Gut)Haben-Betrag in Kontowährung,
|
Credit Amount in Account Currency,Haben-Betrag in Kontowährung,
|
||||||
Voucher Detail No,Belegdetail-Nr.,
|
Voucher Detail No,Belegdetail-Nr.,
|
||||||
Is Opening,Ist Eröffnungsbuchung,
|
Is Opening,Ist Eröffnungsbuchung,
|
||||||
Is Advance,Ist Anzahlung,
|
Is Advance,Ist Anzahlung,
|
||||||
@@ -5699,13 +5699,13 @@ Is Master Data Processed,Werden Stammdaten verarbeitet?,
|
|||||||
Is Master Data Imported,Werden Stammdaten importiert?,
|
Is Master Data Imported,Werden Stammdaten importiert?,
|
||||||
Tally Creditors Account,Tally Gläubigerkonto,
|
Tally Creditors Account,Tally Gläubigerkonto,
|
||||||
Creditors Account set in Tally,Gläubigerkonto in Tally eingestellt,
|
Creditors Account set in Tally,Gläubigerkonto in Tally eingestellt,
|
||||||
Tally Debtors Account,Tally Debtors Account,
|
Tally Debtors Account,Tally Debitorenkonto,
|
||||||
Debtors Account set in Tally,Debitorenkonto in Tally eingestellt,
|
Debtors Account set in Tally,Debitorenkonto in Tally eingestellt,
|
||||||
Tally Company,Tally Company,
|
Tally Company,Tally Unternehmen,
|
||||||
Company Name as per Imported Tally Data,Firmenname gemäß Imported Tally Data,
|
Company Name as per Imported Tally Data,Firmenname gemäß Importierten Tally-Daten,
|
||||||
Default UOM,Standard-UOM,
|
Default UOM,Standard-UOM,
|
||||||
UOM in case unspecified in imported data,"UOM für den Fall, dass in importierten Daten nicht angegeben",
|
UOM in case unspecified in imported data,"UOM für den Fall, dass in importierten Daten nicht angegeben",
|
||||||
ERPNext Company,ERPNext Company,
|
ERPNext Company,ERPNext Unternehmen,
|
||||||
Your Company set in ERPNext,Ihr Unternehmen in ERPNext eingestellt,
|
Your Company set in ERPNext,Ihr Unternehmen in ERPNext eingestellt,
|
||||||
Processed Files,Verarbeitete Dateien,
|
Processed Files,Verarbeitete Dateien,
|
||||||
Parties,Parteien,
|
Parties,Parteien,
|
||||||
@@ -8845,7 +8845,6 @@ Column {0},Spalte {0},
|
|||||||
Field Mapping,Feldzuordnung,
|
Field Mapping,Feldzuordnung,
|
||||||
Not Specified,Keine Angabe,
|
Not Specified,Keine Angabe,
|
||||||
Update Type,Aktualisierungsart,
|
Update Type,Aktualisierungsart,
|
||||||
Dr,Soll,
|
|
||||||
End Time,Endzeit,
|
End Time,Endzeit,
|
||||||
Fetching...,Abrufen ...,
|
Fetching...,Abrufen ...,
|
||||||
"It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account.","Es scheint, dass ein Problem mit der Stripe-Konfiguration des Servers vorliegt. Im Falle eines Fehlers wird der Betrag Ihrem Konto gutgeschrieben.",
|
"It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account.","Es scheint, dass ein Problem mit der Stripe-Konfiguration des Servers vorliegt. Im Falle eines Fehlers wird der Betrag Ihrem Konto gutgeschrieben.",
|
||||||
|
|||||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user