mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 16:34:46 +00:00
Merge branch 'version-14-hotfix' into asdeprledger_cancelled_deprs
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
"determine_address_tax_category_from",
|
"determine_address_tax_category_from",
|
||||||
"column_break_19",
|
"column_break_19",
|
||||||
"add_taxes_from_item_tax_template",
|
"add_taxes_from_item_tax_template",
|
||||||
|
"book_tax_discount_loss",
|
||||||
"print_settings",
|
"print_settings",
|
||||||
"show_inclusive_tax_in_print",
|
"show_inclusive_tax_in_print",
|
||||||
"column_break_12",
|
"column_break_12",
|
||||||
@@ -347,6 +348,13 @@
|
|||||||
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
|
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow multi-currency invoices against single party account "
|
"label": "Allow multi-currency invoices against single party account "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
|
||||||
|
"fieldname": "book_tax_discount_loss",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Book Tax Loss on Early Payment Discount"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -354,7 +362,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-27 21:49:52.538655",
|
"modified": "2023-03-28 09:50:20.375233",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class BankClearance(Document):
|
|||||||
|
|
||||||
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||||
|
|
||||||
loan_disbursements = (
|
query = (
|
||||||
frappe.qb.from_(loan_disbursement)
|
frappe.qb.from_(loan_disbursement)
|
||||||
.select(
|
.select(
|
||||||
ConstantColumn("Loan Disbursement").as_("payment_document"),
|
ConstantColumn("Loan Disbursement").as_("payment_document"),
|
||||||
@@ -90,17 +90,22 @@ class BankClearance(Document):
|
|||||||
ConstantColumn(0).as_("debit"),
|
ConstantColumn(0).as_("debit"),
|
||||||
loan_disbursement.reference_number.as_("cheque_number"),
|
loan_disbursement.reference_number.as_("cheque_number"),
|
||||||
loan_disbursement.reference_date.as_("cheque_date"),
|
loan_disbursement.reference_date.as_("cheque_date"),
|
||||||
|
loan_disbursement.clearance_date.as_("clearance_date"),
|
||||||
loan_disbursement.disbursement_date.as_("posting_date"),
|
loan_disbursement.disbursement_date.as_("posting_date"),
|
||||||
loan_disbursement.applicant.as_("against_account"),
|
loan_disbursement.applicant.as_("against_account"),
|
||||||
)
|
)
|
||||||
.where(loan_disbursement.docstatus == 1)
|
.where(loan_disbursement.docstatus == 1)
|
||||||
.where(loan_disbursement.disbursement_date >= self.from_date)
|
.where(loan_disbursement.disbursement_date >= self.from_date)
|
||||||
.where(loan_disbursement.disbursement_date <= self.to_date)
|
.where(loan_disbursement.disbursement_date <= self.to_date)
|
||||||
.where(loan_disbursement.clearance_date.isnull())
|
|
||||||
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
|
||||||
.orderby(loan_disbursement.disbursement_date)
|
.orderby(loan_disbursement.disbursement_date)
|
||||||
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
||||||
).run(as_dict=1)
|
)
|
||||||
|
|
||||||
|
if not self.include_reconciled_entries:
|
||||||
|
query = query.where(loan_disbursement.clearance_date.isnull())
|
||||||
|
|
||||||
|
loan_disbursements = query.run(as_dict=1)
|
||||||
|
|
||||||
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||||
|
|
||||||
@@ -113,16 +118,19 @@ class BankClearance(Document):
|
|||||||
ConstantColumn(0).as_("credit"),
|
ConstantColumn(0).as_("credit"),
|
||||||
loan_repayment.reference_number.as_("cheque_number"),
|
loan_repayment.reference_number.as_("cheque_number"),
|
||||||
loan_repayment.reference_date.as_("cheque_date"),
|
loan_repayment.reference_date.as_("cheque_date"),
|
||||||
|
loan_repayment.clearance_date.as_("clearance_date"),
|
||||||
loan_repayment.applicant.as_("against_account"),
|
loan_repayment.applicant.as_("against_account"),
|
||||||
loan_repayment.posting_date,
|
loan_repayment.posting_date,
|
||||||
)
|
)
|
||||||
.where(loan_repayment.docstatus == 1)
|
.where(loan_repayment.docstatus == 1)
|
||||||
.where(loan_repayment.clearance_date.isnull())
|
|
||||||
.where(loan_repayment.posting_date >= self.from_date)
|
.where(loan_repayment.posting_date >= self.from_date)
|
||||||
.where(loan_repayment.posting_date <= self.to_date)
|
.where(loan_repayment.posting_date <= self.to_date)
|
||||||
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not self.include_reconciled_entries:
|
||||||
|
query = query.where(loan_repayment.clearance_date.isnull())
|
||||||
|
|
||||||
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
|
||||||
query = query.where((loan_repayment.repay_from_salary == 0))
|
query = query.where((loan_repayment.repay_from_salary == 0))
|
||||||
|
|
||||||
|
|||||||
@@ -325,14 +325,14 @@ def get_template(template_type):
|
|||||||
|
|
||||||
if template_type == "Blank Template":
|
if template_type == "Blank Template":
|
||||||
for root_type in get_root_types():
|
for root_type in get_root_types():
|
||||||
writer.writerow(["", "", "", 1, "", root_type])
|
writer.writerow(["", "", "", "", 1, "", root_type])
|
||||||
|
|
||||||
for account in get_mandatory_group_accounts():
|
for account in get_mandatory_group_accounts():
|
||||||
writer.writerow(["", "", "", 1, account, "Asset"])
|
writer.writerow(["", "", "", "", 1, account, "Asset"])
|
||||||
|
|
||||||
for account_type in get_mandatory_account_types():
|
for account_type in get_mandatory_account_types():
|
||||||
writer.writerow(
|
writer.writerow(
|
||||||
["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
|
["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
writer = get_sample_template(writer)
|
writer = get_sample_template(writer)
|
||||||
|
|||||||
@@ -490,6 +490,8 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
|||||||
conditions.append(gl.company == company)
|
conditions.append(gl.company == company)
|
||||||
conditions.append(gl.account == account)
|
conditions.append(gl.account == account)
|
||||||
conditions.append(gl.is_cancelled == 0)
|
conditions.append(gl.is_cancelled == 0)
|
||||||
|
conditions.append((gl.debit > 0) | (gl.credit > 0))
|
||||||
|
conditions.append((gl.debit_in_account_currency > 0) | (gl.credit_in_account_currency > 0))
|
||||||
if party_type:
|
if party_type:
|
||||||
conditions.append(gl.party_type == party_type)
|
conditions.append(gl.party_type == party_type)
|
||||||
if party:
|
if party:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class JournalEntry(AccountsController):
|
|||||||
self.validate_multi_currency()
|
self.validate_multi_currency()
|
||||||
self.set_amounts_in_company_currency()
|
self.set_amounts_in_company_currency()
|
||||||
self.validate_debit_credit_amount()
|
self.validate_debit_credit_amount()
|
||||||
|
self.set_total_debit_credit()
|
||||||
# Do not validate while importing via data import
|
# Do not validate while importing via data import
|
||||||
if not frappe.flags.in_import:
|
if not frappe.flags.in_import:
|
||||||
self.validate_total_debit_and_credit()
|
self.validate_total_debit_and_credit()
|
||||||
@@ -659,7 +659,6 @@ class JournalEntry(AccountsController):
|
|||||||
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
|
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
|
||||||
|
|
||||||
def validate_total_debit_and_credit(self):
|
def validate_total_debit_and_credit(self):
|
||||||
self.set_total_debit_credit()
|
|
||||||
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
|
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
|
||||||
if self.difference:
|
if self.difference:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
|
|||||||
@@ -245,8 +245,6 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
|
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
|
||||||
party_account_currency, "references");
|
party_account_currency, "references");
|
||||||
|
|
||||||
frm.set_currency_labels(["amount"], company_currency, "deductions");
|
|
||||||
|
|
||||||
cur_frm.set_df_property("source_exchange_rate", "description",
|
cur_frm.set_df_property("source_exchange_rate", "description",
|
||||||
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
|
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
|
||||||
|
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
for ref in self.get("references"):
|
for ref in self.get("references"):
|
||||||
if ref.payment_term and ref.reference_name:
|
if ref.payment_term and ref.reference_name:
|
||||||
key = (ref.payment_term, ref.reference_name)
|
key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
|
||||||
invoice_payment_amount_map.setdefault(key, 0.0)
|
invoice_payment_amount_map.setdefault(key, 0.0)
|
||||||
invoice_payment_amount_map[key] += ref.allocated_amount
|
invoice_payment_amount_map[key] += ref.allocated_amount
|
||||||
|
|
||||||
@@ -424,20 +424,37 @@ class PaymentEntry(AccountsController):
|
|||||||
payment_schedule = frappe.get_all(
|
payment_schedule = frappe.get_all(
|
||||||
"Payment Schedule",
|
"Payment Schedule",
|
||||||
filters={"parent": ref.reference_name},
|
filters={"parent": ref.reference_name},
|
||||||
fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
|
fields=[
|
||||||
|
"paid_amount",
|
||||||
|
"payment_amount",
|
||||||
|
"payment_term",
|
||||||
|
"discount",
|
||||||
|
"outstanding",
|
||||||
|
"discount_type",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
for term in payment_schedule:
|
for term in payment_schedule:
|
||||||
invoice_key = (term.payment_term, ref.reference_name)
|
invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
|
||||||
invoice_paid_amount_map.setdefault(invoice_key, {})
|
invoice_paid_amount_map.setdefault(invoice_key, {})
|
||||||
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
|
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
|
||||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
if not (term.discount_type and term.discount):
|
||||||
term.discount / 100
|
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):
|
||||||
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
|
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
|
||||||
|
|
||||||
|
allocated_amount = self.get_allocated_amount_in_transaction_currency(
|
||||||
|
allocated_amount, key[2], key[1]
|
||||||
|
)
|
||||||
|
|
||||||
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
|
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
|
||||||
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
|
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
|
||||||
|
|
||||||
@@ -472,6 +489,33 @@ class PaymentEntry(AccountsController):
|
|||||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_allocated_amount_in_transaction_currency(
|
||||||
|
self, allocated_amount, reference_doctype, reference_docname
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Payment Entry could be in base currency while reference's payment schedule
|
||||||
|
is always in transaction currency.
|
||||||
|
E.g.
|
||||||
|
* SI with base=INR and currency=USD
|
||||||
|
* SI with payment schedule in USD
|
||||||
|
* PE in INR (accounting done in base currency)
|
||||||
|
"""
|
||||||
|
ref_currency, ref_exchange_rate = frappe.db.get_value(
|
||||||
|
reference_doctype, reference_docname, ["currency", "conversion_rate"]
|
||||||
|
)
|
||||||
|
is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency
|
||||||
|
# PE in different currency
|
||||||
|
reference_is_multi_currency = self.paid_from_account_currency != ref_currency
|
||||||
|
|
||||||
|
if not (is_single_currency and reference_is_multi_currency):
|
||||||
|
return allocated_amount
|
||||||
|
|
||||||
|
allocated_amount = flt(
|
||||||
|
allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
|
||||||
|
)
|
||||||
|
|
||||||
|
return allocated_amount
|
||||||
|
|
||||||
def set_status(self):
|
def set_status(self):
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
self.status = "Cancelled"
|
self.status = "Cancelled"
|
||||||
@@ -1642,7 +1686,14 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_payment_entry(
|
def get_payment_entry(
|
||||||
dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None
|
dt,
|
||||||
|
dn,
|
||||||
|
party_amount=None,
|
||||||
|
bank_account=None,
|
||||||
|
bank_amount=None,
|
||||||
|
party_type=None,
|
||||||
|
payment_type=None,
|
||||||
|
reference_date=None,
|
||||||
):
|
):
|
||||||
reference_doc = None
|
reference_doc = None
|
||||||
doc = frappe.get_doc(dt, dn)
|
doc = frappe.get_doc(dt, dn)
|
||||||
@@ -1669,8 +1720,9 @@ def get_payment_entry(
|
|||||||
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
||||||
)
|
)
|
||||||
|
|
||||||
paid_amount, received_amount, discount_amount = apply_early_payment_discount(
|
reference_date = getdate(reference_date)
|
||||||
paid_amount, received_amount, doc
|
paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount(
|
||||||
|
paid_amount, received_amount, doc, party_account_currency, reference_date
|
||||||
)
|
)
|
||||||
|
|
||||||
pe = frappe.new_doc("Payment Entry")
|
pe = frappe.new_doc("Payment Entry")
|
||||||
@@ -1678,6 +1730,7 @@ def get_payment_entry(
|
|||||||
pe.company = doc.company
|
pe.company = doc.company
|
||||||
pe.cost_center = doc.get("cost_center")
|
pe.cost_center = doc.get("cost_center")
|
||||||
pe.posting_date = nowdate()
|
pe.posting_date = nowdate()
|
||||||
|
pe.reference_date = reference_date
|
||||||
pe.mode_of_payment = doc.get("mode_of_payment")
|
pe.mode_of_payment = doc.get("mode_of_payment")
|
||||||
pe.party_type = party_type
|
pe.party_type = party_type
|
||||||
pe.party = doc.get(scrub(party_type))
|
pe.party = doc.get(scrub(party_type))
|
||||||
@@ -1718,7 +1771,7 @@ def get_payment_entry(
|
|||||||
):
|
):
|
||||||
|
|
||||||
for reference in get_reference_as_per_payment_terms(
|
for reference in get_reference_as_per_payment_terms(
|
||||||
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
|
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||||
):
|
):
|
||||||
pe.append("references", reference)
|
pe.append("references", reference)
|
||||||
else:
|
else:
|
||||||
@@ -1769,16 +1822,17 @@ def get_payment_entry(
|
|||||||
if party_account and bank:
|
if party_account and bank:
|
||||||
pe.set_exchange_rate(ref_doc=reference_doc)
|
pe.set_exchange_rate(ref_doc=reference_doc)
|
||||||
pe.set_amounts()
|
pe.set_amounts()
|
||||||
|
|
||||||
if discount_amount:
|
if discount_amount:
|
||||||
pe.set_gain_or_loss(
|
base_total_discount_loss = 0
|
||||||
account_details={
|
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
||||||
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts)
|
||||||
"cost_center": pe.cost_center
|
|
||||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
set_pending_discount_loss(
|
||||||
"amount": discount_amount * (-1 if payment_type == "Pay" else 1),
|
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||||
}
|
|
||||||
)
|
)
|
||||||
pe.set_difference_amount()
|
|
||||||
|
pe.set_difference_amount()
|
||||||
|
|
||||||
return pe
|
return pe
|
||||||
|
|
||||||
@@ -1889,20 +1943,28 @@ def set_paid_amount_and_received_amount(
|
|||||||
return paid_amount, received_amount
|
return paid_amount, received_amount
|
||||||
|
|
||||||
|
|
||||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
def apply_early_payment_discount(
|
||||||
|
paid_amount, received_amount, doc, party_account_currency, reference_date
|
||||||
|
):
|
||||||
total_discount = 0
|
total_discount = 0
|
||||||
|
valid_discounts = []
|
||||||
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
||||||
has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
|
has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
|
||||||
|
is_multi_currency = party_account_currency != doc.company_currency
|
||||||
|
|
||||||
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
||||||
for term in doc.payment_schedule:
|
for term in doc.payment_schedule:
|
||||||
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
|
||||||
|
|
||||||
if term.discount_type == "Percentage":
|
if term.discount_type == "Percentage":
|
||||||
discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
|
grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
|
||||||
|
discount_amount = flt(grand_total) * (term.discount / 100)
|
||||||
else:
|
else:
|
||||||
discount_amount = term.discount
|
discount_amount = term.discount
|
||||||
|
|
||||||
discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
|
# if accounting is done in the same currency, paid_amount = received_amount
|
||||||
|
conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1
|
||||||
|
discount_amount_in_foreign_currency = discount_amount * conversion_rate
|
||||||
|
|
||||||
if doc.doctype == "Sales Invoice":
|
if doc.doctype == "Sales Invoice":
|
||||||
paid_amount -= discount_amount
|
paid_amount -= discount_amount
|
||||||
@@ -1911,23 +1973,151 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
|
|||||||
received_amount -= discount_amount
|
received_amount -= discount_amount
|
||||||
paid_amount -= discount_amount_in_foreign_currency
|
paid_amount -= discount_amount_in_foreign_currency
|
||||||
|
|
||||||
|
valid_discounts.append({"type": term.discount_type, "discount": term.discount})
|
||||||
total_discount += discount_amount
|
total_discount += discount_amount
|
||||||
|
|
||||||
if total_discount:
|
if total_discount:
|
||||||
money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency"))
|
currency = doc.get("currency") if is_multi_currency else doc.company_currency
|
||||||
|
money = frappe.utils.fmt_money(total_discount, currency=currency)
|
||||||
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
|
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
|
||||||
|
|
||||||
return paid_amount, received_amount, total_discount
|
return paid_amount, received_amount, total_discount, valid_discounts
|
||||||
|
|
||||||
|
|
||||||
|
def set_pending_discount_loss(
|
||||||
|
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||||
|
):
|
||||||
|
# If multi-currency, get base discount amount to adjust with base currency deductions/losses
|
||||||
|
if party_account_currency != doc.company_currency:
|
||||||
|
discount_amount = discount_amount * doc.get("conversion_rate", 1)
|
||||||
|
|
||||||
|
# Avoid considering miniscule losses
|
||||||
|
discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total"))
|
||||||
|
|
||||||
|
# Set base discount amount (discount loss/pending rounding loss) in deductions
|
||||||
|
if discount_amount > 0.0:
|
||||||
|
positive_negative = -1 if pe.payment_type == "Pay" else 1
|
||||||
|
|
||||||
|
# If tax loss booking is enabled, pending loss will be rounding loss.
|
||||||
|
# Otherwise it will be the total discount loss.
|
||||||
|
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
|
||||||
|
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
|
||||||
|
|
||||||
|
pe.set_gain_or_loss(
|
||||||
|
account_details={
|
||||||
|
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||||
|
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||||
|
"amount": discount_amount * positive_negative,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
|
||||||
|
"""Split early payment discount into Income Loss & Tax Loss."""
|
||||||
|
total_discount_percent = get_total_discount_percent(doc, valid_discounts)
|
||||||
|
|
||||||
|
if not total_discount_percent:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent)
|
||||||
|
base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent)
|
||||||
|
|
||||||
|
# Round off total loss rather than individual losses to reduce rounding error
|
||||||
|
return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_total_discount_percent(doc, valid_discounts) -> float:
|
||||||
|
"""Get total percentage and amount discount applied as a percentage."""
|
||||||
|
total_discount_percent = (
|
||||||
|
sum(
|
||||||
|
discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
|
||||||
|
)
|
||||||
|
or 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Operate in percentages only as it makes the income & tax split easier
|
||||||
|
total_discount_amount = (
|
||||||
|
sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount")
|
||||||
|
or 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if total_discount_amount:
|
||||||
|
discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100
|
||||||
|
total_discount_percent += discount_percentage
|
||||||
|
return total_discount_percent
|
||||||
|
|
||||||
|
return total_discount_percent
|
||||||
|
|
||||||
|
|
||||||
|
def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
|
||||||
|
"""Add loss on income discount in base currency."""
|
||||||
|
precision = doc.precision("total")
|
||||||
|
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
|
||||||
|
|
||||||
|
pe.append(
|
||||||
|
"deductions",
|
||||||
|
{
|
||||||
|
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
||||||
|
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||||
|
"amount": flt(base_loss_on_income, precision),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return base_loss_on_income # Return loss without rounding
|
||||||
|
|
||||||
|
|
||||||
|
def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||||
|
"""Add loss on tax discount in base currency."""
|
||||||
|
tax_discount_loss = {}
|
||||||
|
base_total_tax_loss = 0
|
||||||
|
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
|
||||||
|
|
||||||
|
# The same account head could be used more than once
|
||||||
|
for tax in doc.get("taxes", []):
|
||||||
|
base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
|
||||||
|
total_discount_percentage / 100
|
||||||
|
)
|
||||||
|
|
||||||
|
account = tax.get("account_head")
|
||||||
|
if not tax_discount_loss.get(account):
|
||||||
|
tax_discount_loss[account] = base_tax_loss
|
||||||
|
else:
|
||||||
|
tax_discount_loss[account] += base_tax_loss
|
||||||
|
|
||||||
|
for account, loss in tax_discount_loss.items():
|
||||||
|
base_total_tax_loss += loss
|
||||||
|
if loss == 0.0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pe.append(
|
||||||
|
"deductions",
|
||||||
|
{
|
||||||
|
"account": account,
|
||||||
|
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||||
|
"amount": flt(loss, precision),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return base_total_tax_loss # Return loss without rounding
|
||||||
|
|
||||||
|
|
||||||
def get_reference_as_per_payment_terms(
|
def get_reference_as_per_payment_terms(
|
||||||
payment_schedule, dt, dn, doc, grand_total, outstanding_amount
|
payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||||
):
|
):
|
||||||
references = []
|
references = []
|
||||||
|
is_multi_currency_acc = (doc.currency != doc.company_currency) and (
|
||||||
|
party_account_currency != doc.company_currency
|
||||||
|
)
|
||||||
|
|
||||||
for payment_term in payment_schedule:
|
for payment_term in payment_schedule:
|
||||||
payment_term_outstanding = flt(
|
payment_term_outstanding = flt(
|
||||||
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
|
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
|
||||||
)
|
)
|
||||||
|
if not is_multi_currency_acc:
|
||||||
|
# If accounting is done in company currency for multi-currency transaction
|
||||||
|
payment_term_outstanding = flt(
|
||||||
|
payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
|
||||||
|
)
|
||||||
|
|
||||||
if payment_term_outstanding:
|
if payment_term_outstanding:
|
||||||
references.append(
|
references.append(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||||
@@ -256,10 +256,25 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
si.save()
|
si.save()
|
||||||
|
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||||
|
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||||
|
|
||||||
|
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
|
||||||
|
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0)
|
||||||
|
self.assertEqual(pe_with_tax_loss.paid_amount, 212.4)
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||||
|
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 236.0)
|
||||||
|
self.assertEqual(pe.paid_amount, 212.4)
|
||||||
|
self.assertEqual(pe.deductions[0].amount, 23.6)
|
||||||
|
|
||||||
pe.submit()
|
pe.submit()
|
||||||
si.load_from_db()
|
si.load_from_db()
|
||||||
|
|
||||||
@@ -269,6 +284,190 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
self.assertEqual(si.payment_schedule[0].outstanding, 0)
|
self.assertEqual(si.payment_schedule[0].outstanding, 0)
|
||||||
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
|
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
|
||||||
|
|
||||||
|
def test_payment_entry_against_payment_terms_with_discount_amount(self):
|
||||||
|
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||||
|
|
||||||
|
si.payment_terms_template = "Test Discount Amount Template"
|
||||||
|
create_payment_terms_template_with_discount(
|
||||||
|
name="30 Credit Days with Rs.50 Discount",
|
||||||
|
discount_type="Amount",
|
||||||
|
discount=50,
|
||||||
|
template_name="Test Discount Amount Template",
|
||||||
|
)
|
||||||
|
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||||
|
|
||||||
|
si.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account Service Tax - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Service Tax",
|
||||||
|
"rate": 18,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
# Set reference date past discount cut off date
|
||||||
|
pe_1 = get_payment_entry(
|
||||||
|
"Sales Invoice",
|
||||||
|
si.name,
|
||||||
|
bank_account="_Test Cash - _TC",
|
||||||
|
reference_date=frappe.utils.add_days(si.posting_date, 2),
|
||||||
|
)
|
||||||
|
self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied
|
||||||
|
|
||||||
|
# Test if tax loss is booked on enabling configuration
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||||
|
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||||
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 236.0)
|
||||||
|
self.assertEqual(pe.paid_amount, 186)
|
||||||
|
self.assertEqual(pe.deductions[0].amount, 50.0)
|
||||||
|
|
||||||
|
pe.submit()
|
||||||
|
si.load_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
|
||||||
|
self.assertEqual(si.payment_schedule[0].paid_amount, 186)
|
||||||
|
self.assertEqual(si.payment_schedule[0].outstanding, 0)
|
||||||
|
self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{
|
||||||
|
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||||
|
"book_tax_discount_loss": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
1. Multi-currency SI with single currency accounting (company currency)
|
||||||
|
2. PE with early payment discount
|
||||||
|
3. Test if Paid Amount is calculated in company currency
|
||||||
|
4. Test if deductions are calculated in company currency
|
||||||
|
|
||||||
|
SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency.
|
||||||
|
"""
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer="_Test Customer",
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=50,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
create_payment_terms_template_with_discount()
|
||||||
|
si.payment_terms_template = "Test Discount Template"
|
||||||
|
|
||||||
|
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
pe = get_payment_entry(
|
||||||
|
"Sales Invoice",
|
||||||
|
si.name,
|
||||||
|
bank_account="_Test Bank - _TC",
|
||||||
|
)
|
||||||
|
pe.reference_no = si.name
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
|
||||||
|
# Early payment discount loss on income
|
||||||
|
self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency
|
||||||
|
self.assertEqual(pe.received_amount, 4500.0)
|
||||||
|
self.assertEqual(pe.deductions[0].amount, 500.0)
|
||||||
|
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||||
|
self.assertEqual(pe.difference_amount, 0.0)
|
||||||
|
|
||||||
|
pe.insert()
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
expected_gle = dict(
|
||||||
|
(d[0], d)
|
||||||
|
for d in [
|
||||||
|
["Debtors - _TC", 0, 5000, si.name],
|
||||||
|
["_Test Bank - _TC", 4500, 0, None],
|
||||||
|
["Write Off - _TC", 500.0, 0, None],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.validate_gl_entries(pe.name, expected_gle)
|
||||||
|
|
||||||
|
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||||
|
self.assertEqual(outstanding_amount, 0)
|
||||||
|
|
||||||
|
def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self):
|
||||||
|
"""
|
||||||
|
1. Multi-currency SI with multi-currency accounting
|
||||||
|
2. PE with early payment discount and also exchange loss
|
||||||
|
3. Test if Paid Amount is calculated in transaction currency
|
||||||
|
4. Test if deductions are calculated in base/company currency
|
||||||
|
5. Test if exchange loss is reflected in difference
|
||||||
|
"""
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer="_Test Customer USD",
|
||||||
|
debit_to="_Test Receivable USD - _TC",
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=50,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
create_payment_terms_template_with_discount()
|
||||||
|
si.payment_terms_template = "Test Discount Template"
|
||||||
|
|
||||||
|
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
pe = get_payment_entry(
|
||||||
|
"Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
|
||||||
|
)
|
||||||
|
pe.reference_no = si.name
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
|
||||||
|
# Early payment discount loss on income
|
||||||
|
self.assertEqual(pe.paid_amount, 90.0)
|
||||||
|
self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss)
|
||||||
|
self.assertEqual(pe.deductions[0].amount, 500.0)
|
||||||
|
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||||
|
|
||||||
|
# Exchange loss
|
||||||
|
self.assertEqual(pe.difference_amount, 300.0)
|
||||||
|
|
||||||
|
pe.append(
|
||||||
|
"deductions",
|
||||||
|
{
|
||||||
|
"account": "_Test Exchange Gain/Loss - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"amount": 300.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pe.insert()
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pe.difference_amount, 0.0)
|
||||||
|
|
||||||
|
expected_gle = dict(
|
||||||
|
(d[0], d)
|
||||||
|
for d in [
|
||||||
|
["_Test Receivable USD - _TC", 0, 5000, si.name],
|
||||||
|
["_Test Bank - _TC", 4200, 0, None],
|
||||||
|
["Write Off - _TC", 500.0, 0, None],
|
||||||
|
["_Test Exchange Gain/Loss - _TC", 300.0, 0, None],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.validate_gl_entries(pe.name, expected_gle)
|
||||||
|
|
||||||
|
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||||
|
self.assertEqual(outstanding_amount, 0)
|
||||||
|
|
||||||
def test_payment_against_purchase_invoice_to_check_status(self):
|
def test_payment_against_purchase_invoice_to_check_status(self):
|
||||||
pi = make_purchase_invoice(
|
pi = make_purchase_invoice(
|
||||||
supplier="_Test Supplier USD",
|
supplier="_Test Supplier USD",
|
||||||
@@ -839,24 +1038,27 @@ def create_payment_terms_template():
|
|||||||
).insert()
|
).insert()
|
||||||
|
|
||||||
|
|
||||||
def create_payment_terms_template_with_discount():
|
def create_payment_terms_template_with_discount(
|
||||||
|
name=None, discount_type=None, discount=None, template_name=None
|
||||||
|
):
|
||||||
|
create_payment_term(name or "30 Credit Days with 10% Discount")
|
||||||
|
template_name = template_name or "Test Discount Template"
|
||||||
|
|
||||||
create_payment_term("30 Credit Days with 10% Discount")
|
if not frappe.db.exists("Payment Terms Template", template_name):
|
||||||
|
frappe.get_doc(
|
||||||
if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
|
|
||||||
payment_term_template = frappe.get_doc(
|
|
||||||
{
|
{
|
||||||
"doctype": "Payment Terms Template",
|
"doctype": "Payment Terms Template",
|
||||||
"template_name": "Test Discount Template",
|
"template_name": template_name,
|
||||||
"allocate_payment_based_on_payment_terms": 1,
|
"allocate_payment_based_on_payment_terms": 1,
|
||||||
"terms": [
|
"terms": [
|
||||||
{
|
{
|
||||||
"doctype": "Payment Terms Template Detail",
|
"doctype": "Payment Terms Template Detail",
|
||||||
"payment_term": "30 Credit Days with 10% Discount",
|
"payment_term": name or "30 Credit Days with 10% Discount",
|
||||||
"invoice_portion": 100,
|
"invoice_portion": 100,
|
||||||
"credit_days_based_on": "Day(s) after invoice date",
|
"credit_days_based_on": "Day(s) after invoice date",
|
||||||
"credit_days": 2,
|
"credit_days": 2,
|
||||||
"discount": 10,
|
"discount_type": discount_type or "Percentage",
|
||||||
|
"discount": discount or 10,
|
||||||
"discount_validity_based_on": "Day(s) after invoice date",
|
"discount_validity_based_on": "Day(s) after invoice date",
|
||||||
"discount_validity": 1,
|
"discount_validity": 1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"creation": "2016-06-15 15:56:30.815503",
|
"creation": "2016-06-15 15:56:30.815503",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"account",
|
"account",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
@@ -17,9 +18,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Account",
|
"label": "Account",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"reqd": 1,
|
"reqd": 1
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "cost_center",
|
"fieldname": "cost_center",
|
||||||
@@ -28,37 +27,30 @@
|
|||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center",
|
"options": "Cost Center",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"reqd": 1,
|
"reqd": 1
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Amount",
|
"label": "Amount (Company Currency)",
|
||||||
"reqd": 1,
|
"options": "Company:company:default_currency",
|
||||||
"show_days": 1,
|
"reqd": 1
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_2",
|
"fieldname": "column_break_2",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Description",
|
"label": "Description"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-12 20:38:08.110674",
|
"modified": "2023-03-06 07:11:57.739619",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Deduction",
|
"name": "Payment Entry Deduction",
|
||||||
@@ -66,5 +58,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
||||||
@@ -272,4 +272,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
frappe.ui.form.on('Payment Reconciliation Allocation', {
|
||||||
|
allocated_amount: function(frm, cdt, cdn) {
|
||||||
|
let row = locals[cdt][cdn];
|
||||||
|
// filter invoice
|
||||||
|
let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number));
|
||||||
|
// filter payment
|
||||||
|
let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name));
|
||||||
|
|
||||||
|
frm.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: 'calculate_difference_on_allocation_change',
|
||||||
|
args: {
|
||||||
|
payment_entry: payment,
|
||||||
|
invoice: invoice,
|
||||||
|
allocated_amount: row.allocated_amount
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
if (r.message) {
|
||||||
|
row.difference_amount = r.message;
|
||||||
|
frm.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));
|
extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));
|
||||||
|
|||||||
@@ -233,6 +233,15 @@ class PaymentReconciliation(Document):
|
|||||||
|
|
||||||
return difference_amount
|
return difference_amount
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
|
||||||
|
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
|
||||||
|
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
|
||||||
|
new_difference_amount = self.get_difference_amount(
|
||||||
|
payment_entry[0], invoice[0], allocated_amount
|
||||||
|
)
|
||||||
|
return new_difference_amount
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def allocate_entries(self, args):
|
def allocate_entries(self, args):
|
||||||
self.validate_entries()
|
self.validate_entries()
|
||||||
|
|||||||
@@ -674,7 +674,7 @@ def get_bin_qty(item_code, warehouse):
|
|||||||
|
|
||||||
def get_pos_reserved_qty(item_code, warehouse):
|
def get_pos_reserved_qty(item_code, warehouse):
|
||||||
reserved_qty = frappe.db.sql(
|
reserved_qty = frappe.db.sql(
|
||||||
"""select sum(p_item.qty) as qty
|
"""select sum(p_item.stock_qty) as qty
|
||||||
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
|
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
|
||||||
where p.name = p_item.parent
|
where p.name = p_item.parent
|
||||||
and ifnull(p.consolidated_invoice, '') = ''
|
and ifnull(p.consolidated_invoice, '') = ''
|
||||||
|
|||||||
@@ -82,7 +82,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
|
|
||||||
if(doc.docstatus == 1 && doc.outstanding_amount != 0
|
if(doc.docstatus == 1 && doc.outstanding_amount != 0
|
||||||
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
|
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
|
||||||
this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create'));
|
this.frm.add_custom_button(
|
||||||
|
__('Payment'),
|
||||||
|
() => this.make_payment_entry(),
|
||||||
|
__('Create')
|
||||||
|
);
|
||||||
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.validate_expense_account()
|
self.validate_expense_account()
|
||||||
self.set_against_expense_account()
|
self.set_against_expense_account()
|
||||||
self.validate_write_off_account()
|
self.validate_write_off_account()
|
||||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
|
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||||
self.create_remarks()
|
self.create_remarks()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.validate_purchase_receipt_if_update_stock()
|
self.validate_purchase_receipt_if_update_stock()
|
||||||
@@ -232,7 +232,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
|
cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
|
||||||
and not self.is_return
|
and not self.is_return
|
||||||
and not self.is_internal_supplier
|
and not self.is_internal_supplier
|
||||||
):
|
):
|
||||||
@@ -581,6 +581,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
self.make_supplier_gl_entry(gl_entries)
|
self.make_supplier_gl_entry(gl_entries)
|
||||||
self.make_item_gl_entries(gl_entries)
|
self.make_item_gl_entries(gl_entries)
|
||||||
|
self.make_precision_loss_gl_entry(gl_entries)
|
||||||
|
|
||||||
if self.check_asset_cwip_enabled():
|
if self.check_asset_cwip_enabled():
|
||||||
self.get_asset_gl_entry(gl_entries)
|
self.get_asset_gl_entry(gl_entries)
|
||||||
@@ -975,6 +976,28 @@ class PurchaseInvoice(BuyingController):
|
|||||||
item.item_tax_amount, item.precision("item_tax_amount")
|
item.item_tax_amount, item.precision("item_tax_amount")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def make_precision_loss_gl_entry(self, gl_entries):
|
||||||
|
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||||
|
self.company, "Purchase Invoice", self.name
|
||||||
|
)
|
||||||
|
|
||||||
|
precision_loss = self.get("base_net_total") - flt(
|
||||||
|
self.get("net_total") * self.conversion_rate, self.precision("net_total")
|
||||||
|
)
|
||||||
|
|
||||||
|
if precision_loss:
|
||||||
|
gl_entries.append(
|
||||||
|
self.get_gl_dict(
|
||||||
|
{
|
||||||
|
"account": round_off_account,
|
||||||
|
"against": self.supplier,
|
||||||
|
"credit": precision_loss,
|
||||||
|
"cost_center": self.cost_center or round_off_cost_center,
|
||||||
|
"remarks": _("Net total calculation precision loss"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_asset_gl_entry(self, gl_entries):
|
def get_asset_gl_entry(self, gl_entries):
|
||||||
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
||||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||||
|
|||||||
@@ -93,9 +93,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
|
|
||||||
if (doc.docstatus == 1 && doc.outstanding_amount!=0
|
if (doc.docstatus == 1 && doc.outstanding_amount!=0
|
||||||
&& !(cint(doc.is_return) && doc.return_against)) {
|
&& !(cint(doc.is_return) && doc.return_against)) {
|
||||||
cur_frm.add_custom_button(__('Payment'),
|
this.frm.add_custom_button(
|
||||||
this.make_payment_entry, __('Create'));
|
__('Payment'),
|
||||||
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
() => this.make_payment_entry(),
|
||||||
|
__('Create')
|
||||||
|
);
|
||||||
|
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(doc.docstatus==1 && !doc.is_return) {
|
if(doc.docstatus==1 && !doc.is_return) {
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
self.set_against_income_account()
|
self.set_against_income_account()
|
||||||
self.validate_time_sheets_are_submitted()
|
self.validate_time_sheets_are_submitted()
|
||||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
|
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
|
||||||
if not self.is_return:
|
if not self.is_return:
|
||||||
self.validate_serial_numbers()
|
self.validate_serial_numbers()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -174,6 +174,9 @@ def _get_party_details(
|
|||||||
party_type, party.name, "tax_withholding_category"
|
party_type, party.name, "tax_withholding_category"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not party_details.get("tax_category") and pos_profile:
|
||||||
|
party_details["tax_category"] = frappe.get_value("POS Profile", pos_profile, "tax_category")
|
||||||
|
|
||||||
return party_details
|
return party_details
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.utils import getdate, nowdate
|
from frappe.utils import getdate, nowdate
|
||||||
|
|
||||||
|
|
||||||
@@ -91,4 +92,65 @@ def get_entries(filters):
|
|||||||
as_list=1,
|
as_list=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate()))
|
# Loan Disbursement
|
||||||
|
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(loan_disbursement)
|
||||||
|
.select(
|
||||||
|
ConstantColumn("Loan Disbursement").as_("payment_document_type"),
|
||||||
|
loan_disbursement.name.as_("payment_entry"),
|
||||||
|
loan_disbursement.disbursement_date.as_("posting_date"),
|
||||||
|
loan_disbursement.reference_number.as_("cheque_no"),
|
||||||
|
loan_disbursement.clearance_date.as_("clearance_date"),
|
||||||
|
loan_disbursement.applicant.as_("against"),
|
||||||
|
-loan_disbursement.disbursed_amount.as_("amount"),
|
||||||
|
)
|
||||||
|
.where(loan_disbursement.docstatus == 1)
|
||||||
|
.where(loan_disbursement.disbursement_date >= filters["from_date"])
|
||||||
|
.where(loan_disbursement.disbursement_date <= filters["to_date"])
|
||||||
|
.where(loan_disbursement.disbursement_account == filters["account"])
|
||||||
|
.orderby(loan_disbursement.disbursement_date, order=frappe.qb.desc)
|
||||||
|
.orderby(loan_disbursement.name, order=frappe.qb.desc)
|
||||||
|
)
|
||||||
|
|
||||||
|
if filters.get("from_date"):
|
||||||
|
query = query.where(loan_disbursement.disbursement_date >= filters["from_date"])
|
||||||
|
if filters.get("to_date"):
|
||||||
|
query = query.where(loan_disbursement.disbursement_date <= filters["to_date"])
|
||||||
|
|
||||||
|
loan_disbursements = query.run(as_list=1)
|
||||||
|
|
||||||
|
# Loan Repayment
|
||||||
|
loan_repayment = frappe.qb.DocType("Loan Repayment")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(loan_repayment)
|
||||||
|
.select(
|
||||||
|
ConstantColumn("Loan Repayment").as_("payment_document_type"),
|
||||||
|
loan_repayment.name.as_("payment_entry"),
|
||||||
|
loan_repayment.posting_date.as_("posting_date"),
|
||||||
|
loan_repayment.reference_number.as_("cheque_no"),
|
||||||
|
loan_repayment.clearance_date.as_("clearance_date"),
|
||||||
|
loan_repayment.applicant.as_("against"),
|
||||||
|
loan_repayment.amount_paid.as_("amount"),
|
||||||
|
)
|
||||||
|
.where(loan_repayment.docstatus == 1)
|
||||||
|
.where(loan_repayment.posting_date >= filters["from_date"])
|
||||||
|
.where(loan_repayment.posting_date <= filters["to_date"])
|
||||||
|
.where(loan_repayment.payment_account == filters["account"])
|
||||||
|
.orderby(loan_repayment.posting_date, order=frappe.qb.desc)
|
||||||
|
.orderby(loan_repayment.name, order=frappe.qb.desc)
|
||||||
|
)
|
||||||
|
|
||||||
|
if filters.get("from_date"):
|
||||||
|
query = query.where(loan_repayment.posting_date >= filters["from_date"])
|
||||||
|
if filters.get("to_date"):
|
||||||
|
query = query.where(loan_repayment.posting_date <= filters["to_date"])
|
||||||
|
|
||||||
|
loan_repayments = query.run(as_list=1)
|
||||||
|
|
||||||
|
return sorted(
|
||||||
|
journal_entries + payment_entries + loan_disbursements + loan_repayments,
|
||||||
|
key=lambda k: k[2] or getdate(nowdate()),
|
||||||
|
)
|
||||||
|
|||||||
@@ -58,9 +58,8 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
{
|
{
|
||||||
"fieldname":"party_type",
|
"fieldname":"party_type",
|
||||||
"label": __("Party Type"),
|
"label": __("Party Type"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Autocomplete",
|
||||||
"options": "Party Type",
|
options: Object.keys(frappe.boot.party_account_types),
|
||||||
"default": "",
|
|
||||||
on_change: function() {
|
on_change: function() {
|
||||||
frappe.query_report.set_filter_value('party', "");
|
frappe.query_report.set_filter_value('party', "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,12 +451,6 @@ def reconcile_against_document(args): # nosemgrep
|
|||||||
else:
|
else:
|
||||||
update_reference_in_payment_entry(entry, doc, do_not_save=True)
|
update_reference_in_payment_entry(entry, doc, do_not_save=True)
|
||||||
|
|
||||||
if doc.doctype == "Journal Entry":
|
|
||||||
try:
|
|
||||||
doc.validate_total_debit_and_credit()
|
|
||||||
except Exception as validation_exception:
|
|
||||||
raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception
|
|
||||||
|
|
||||||
doc.save(ignore_permissions=True)
|
doc.save(ignore_permissions=True)
|
||||||
# re-submit advance entry
|
# re-submit advance entry
|
||||||
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
|
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
|
||||||
|
|||||||
@@ -469,6 +469,9 @@ frappe.ui.form.on('Asset', {
|
|||||||
} else {
|
} else {
|
||||||
frm.set_value('purchase_date', purchase_doc.posting_date);
|
frm.set_value('purchase_date', purchase_doc.posting_date);
|
||||||
}
|
}
|
||||||
|
if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) {
|
||||||
|
frm.set_value('available_for_use_date', frm.doc.purchase_date);
|
||||||
|
}
|
||||||
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
|
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
doctype_field = frappe.scrub(doctype)
|
doctype_field = frappe.scrub(doctype)
|
||||||
|
|||||||
@@ -81,6 +81,9 @@
|
|||||||
"options": "ACC-ASS-.YYYY.-"
|
"options": "ACC-ASS-.YYYY.-"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "item_code",
|
||||||
|
"fetch_from": "item_code.item_name",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "asset_name",
|
"fieldname": "asset_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -527,7 +530,7 @@
|
|||||||
"table_fieldname": "accounts"
|
"table_fieldname": "accounts"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2023-01-25 17:45:48.649543",
|
"modified": "2023-03-30 15:07:41.542374",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
|
|||||||
@@ -218,10 +218,16 @@ def notify_depr_entry_posting_error(failed_asset_names):
|
|||||||
asset_links = get_comma_separated_asset_links(failed_asset_names)
|
asset_links = get_comma_separated_asset_links(failed_asset_names)
|
||||||
|
|
||||||
message = (
|
message = (
|
||||||
_("Hi,")
|
_("Hello,")
|
||||||
+ "<br>"
|
+ "<br><br>"
|
||||||
+ _("The following assets have failed to post depreciation entries: {0}").format(asset_links)
|
+ _("The following assets have failed to automatically post depreciation entries: {0}").format(
|
||||||
|
asset_links
|
||||||
|
)
|
||||||
+ "."
|
+ "."
|
||||||
|
+ "<br><br>"
|
||||||
|
+ _(
|
||||||
|
"Please raise a support ticket and share this email, or forward this email to your development team so that they can find the issue in the developer console by manually creating the depreciation entry via the asset's depreciation schedule table."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.sendmail(recipients=recipients, subject=subject, message=message)
|
frappe.sendmail(recipients=recipients, subject=subject, message=message)
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ def calculate_next_due_date(
|
|||||||
next_due_date = add_years(start_date, 1)
|
next_due_date = add_years(start_date, 1)
|
||||||
if periodicity == "2 Yearly":
|
if periodicity == "2 Yearly":
|
||||||
next_due_date = add_years(start_date, 2)
|
next_due_date = add_years(start_date, 2)
|
||||||
|
if periodicity == "3 Yearly":
|
||||||
|
next_due_date = add_years(start_date, 3)
|
||||||
if periodicity == "Quarterly":
|
if periodicity == "Quarterly":
|
||||||
next_due_date = add_months(start_date, 3)
|
next_due_date = add_months(start_date, 3)
|
||||||
if end_date and (
|
if end_date and (
|
||||||
|
|||||||
@@ -1,664 +1,156 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"autoname": "",
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2017-10-20 07:10:55.903571",
|
"creation": "2017-10-20 07:10:55.903571",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Document",
|
"document_type": "Document",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"maintenance_task",
|
||||||
|
"maintenance_type",
|
||||||
|
"column_break_2",
|
||||||
|
"maintenance_status",
|
||||||
|
"section_break_2",
|
||||||
|
"start_date",
|
||||||
|
"periodicity",
|
||||||
|
"column_break_4",
|
||||||
|
"end_date",
|
||||||
|
"certificate_required",
|
||||||
|
"section_break_9",
|
||||||
|
"assign_to",
|
||||||
|
"column_break_10",
|
||||||
|
"assign_to_name",
|
||||||
|
"section_break_10",
|
||||||
|
"next_due_date",
|
||||||
|
"column_break_14",
|
||||||
|
"last_completion_date",
|
||||||
|
"section_break_7",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "maintenance_task",
|
"fieldname": "maintenance_task",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 1,
|
"in_filter": 1,
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Maintenance Task",
|
"label": "Maintenance Task",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "maintenance_type",
|
"fieldname": "maintenance_type",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Maintenance Type",
|
"label": "Maintenance Type",
|
||||||
"length": 0,
|
"options": "Preventive Maintenance\nCalibration"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Preventive Maintenance\nCalibration",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_2",
|
"fieldname": "column_break_2",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "",
|
|
||||||
"fieldname": "maintenance_status",
|
"fieldname": "maintenance_status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Maintenance Status",
|
"label": "Maintenance Status",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Planned\nOverdue\nCancelled",
|
"options": "Planned\nOverdue\nCancelled",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "section_break_2",
|
"fieldname": "section_break_2",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "Today",
|
"default": "Today",
|
||||||
"fieldname": "start_date",
|
"fieldname": "start_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Start Date",
|
"label": "Start Date",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "periodicity",
|
"fieldname": "periodicity",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Periodicity",
|
"label": "Periodicity",
|
||||||
"length": 0,
|
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly",
|
||||||
"no_copy": 0,
|
"reqd": 1
|
||||||
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_4",
|
"fieldname": "column_break_4",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "end_date",
|
"fieldname": "end_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
"label": "End Date"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "End Date",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "certificate_required",
|
"fieldname": "certificate_required",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Certificate Required",
|
"label": "Certificate Required",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 1,
|
"search_index": 1,
|
||||||
"set_only_once": 1,
|
"set_only_once": 1
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "section_break_9",
|
"fieldname": "section_break_9",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "assign_to",
|
"fieldname": "assign_to",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Assign To",
|
"label": "Assign To",
|
||||||
"length": 0,
|
"options": "User"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "User",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_10",
|
"fieldname": "column_break_10",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_from": "assign_to.full_name",
|
"fetch_from": "assign_to.full_name",
|
||||||
"fieldname": "assign_to_name",
|
"fieldname": "assign_to_name",
|
||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"hidden": 0,
|
"label": "Assign to Name"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Assign to Name",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "section_break_10",
|
"fieldname": "section_break_10",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "next_due_date",
|
"fieldname": "next_due_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Next Due Date"
|
||||||
"label": "Next Due Date",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_14",
|
"fieldname": "column_break_14",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "last_completion_date",
|
"fieldname": "last_completion_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Last Completion Date"
|
||||||
"label": "Last Completion Date",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "section_break_7",
|
"fieldname": "section_break_7",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Text Editor",
|
"fieldtype": "Text Editor",
|
||||||
"hidden": 0,
|
"label": "Description"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Description",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2018-06-18 16:12:04.330021",
|
"modified": "2023-03-23 07:03:07.113452",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Maintenance Task",
|
"name": "Asset Maintenance Task",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC"
|
||||||
"track_changes": 0,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ frappe.ui.form.on('Asset Value Adjustment', {
|
|||||||
frm.call({
|
frm.call({
|
||||||
method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation",
|
method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation",
|
||||||
args: {
|
args: {
|
||||||
asset: frm.doc.asset,
|
asset_name: frm.doc.asset,
|
||||||
finance_book: frm.doc.finance_book
|
finance_book: frm.doc.finance_book
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
|
|||||||
@@ -236,7 +236,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
|||||||
this.make_purchase_invoice, __('Create'));
|
this.make_purchase_invoice, __('Create'));
|
||||||
|
|
||||||
if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
|
if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
|
||||||
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
|
this.frm.add_custom_button(
|
||||||
|
__('Payment'),
|
||||||
|
() => this.make_payment_entry(),
|
||||||
|
__('Create')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(flt(doc.per_billed) < 100) {
|
if(flt(doc.per_billed) < 100) {
|
||||||
|
|||||||
@@ -113,7 +113,10 @@ class RequestforQuotation(BuyingController):
|
|||||||
|
|
||||||
def get_link(self):
|
def get_link(self):
|
||||||
# RFQ link for supplier portal
|
# RFQ link for supplier portal
|
||||||
return get_url("/app/request-for-quotation/" + self.name)
|
route = frappe.db.get_value(
|
||||||
|
"Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"]
|
||||||
|
)
|
||||||
|
return get_url("/app/{0}/".format(route) + self.name)
|
||||||
|
|
||||||
def update_supplier_part_no(self, supplier):
|
def update_supplier_part_no(self, supplier):
|
||||||
self.vendor = supplier
|
self.vendor = supplier
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ frappe.ui.form.on("Supplier", {
|
|||||||
// custom buttons
|
// custom buttons
|
||||||
frm.add_custom_button(__('Accounting Ledger'), function () {
|
frm.add_custom_button(__('Accounting Ledger'), function () {
|
||||||
frappe.set_route('query-report', 'General Ledger',
|
frappe.set_route('query-report', 'General Ledger',
|
||||||
{ party_type: 'Supplier', party: frm.doc.name });
|
{ party_type: 'Supplier', party: frm.doc.name, party_name: frm.doc.supplier_name });
|
||||||
}, __("View"));
|
}, __("View"));
|
||||||
|
|
||||||
frm.add_custom_button(__('Accounts Payable'), function () {
|
frm.add_custom_button(__('Accounts Payable'), function () {
|
||||||
|
|||||||
@@ -515,6 +515,8 @@ class AccountsController(TransactionBase):
|
|||||||
parent_dict.update({"customer": parent_dict.get("party_name")})
|
parent_dict.update({"customer": parent_dict.get("party_name")})
|
||||||
|
|
||||||
self.pricing_rules = []
|
self.pricing_rules = []
|
||||||
|
basic_item_details_map = {}
|
||||||
|
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if item.get("item_code"):
|
if item.get("item_code"):
|
||||||
args = parent_dict.copy()
|
args = parent_dict.copy()
|
||||||
@@ -533,7 +535,17 @@ class AccountsController(TransactionBase):
|
|||||||
if self.get("is_subcontracted"):
|
if self.get("is_subcontracted"):
|
||||||
args["is_subcontracted"] = self.is_subcontracted
|
args["is_subcontracted"] = self.is_subcontracted
|
||||||
|
|
||||||
ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
|
basic_details = basic_item_details_map.get(item.item_code)
|
||||||
|
ret, basic_item_details = get_item_details(
|
||||||
|
args,
|
||||||
|
self,
|
||||||
|
for_validate=True,
|
||||||
|
overwrite_warehouse=False,
|
||||||
|
return_basic_details=True,
|
||||||
|
basic_details=basic_details,
|
||||||
|
)
|
||||||
|
|
||||||
|
basic_item_details_map.setdefault(item.item_code, basic_item_details)
|
||||||
|
|
||||||
for fieldname, value in ret.items():
|
for fieldname, value in ret.items():
|
||||||
if item.meta.get_field(fieldname) and value is not None:
|
if item.meta.get_field(fieldname) and value is not None:
|
||||||
@@ -1232,7 +1244,7 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
|
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
|
||||||
from erpnext.controllers.status_updater import get_allowance_for
|
from erpnext.controllers.status_updater import get_allowance_for
|
||||||
|
|
||||||
item_allowance = {}
|
item_allowance = {}
|
||||||
@@ -1245,17 +1257,20 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
total_overbilled_amt = 0.0
|
total_overbilled_amt = 0.0
|
||||||
|
|
||||||
|
reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
|
||||||
|
reference_details = self.get_billing_reference_details(
|
||||||
|
reference_names, ref_dt + " Item", based_on
|
||||||
|
)
|
||||||
|
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if not item.get(item_ref_dn):
|
if not item.get(item_ref_dn):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ref_amt = flt(
|
ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
|
||||||
frappe.db.get_value(ref_dt + " Item", item.get(item_ref_dn), based_on),
|
|
||||||
self.precision(based_on, item),
|
|
||||||
)
|
|
||||||
if not ref_amt:
|
if not ref_amt:
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("System will not check overbilling since amount for Item {0} in {1} is zero").format(
|
_("System will not check over billing since amount for Item {0} in {1} is zero").format(
|
||||||
item.item_code, ref_dt
|
item.item_code, ref_dt
|
||||||
),
|
),
|
||||||
title=_("Warning"),
|
title=_("Warning"),
|
||||||
@@ -1302,6 +1317,16 @@ class AccountsController(TransactionBase):
|
|||||||
alert=True,
|
alert=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_billing_reference_details(self, reference_names, reference_doctype, based_on):
|
||||||
|
return frappe._dict(
|
||||||
|
frappe.get_all(
|
||||||
|
reference_doctype,
|
||||||
|
filters={"name": ("in", reference_names)},
|
||||||
|
fields=["name", based_on],
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
|
def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
|
||||||
"""
|
"""
|
||||||
Returns Sum of Amount of
|
Returns Sum of Amount of
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ class StatusUpdater(Document):
|
|||||||
ifnull((select
|
ifnull((select
|
||||||
ifnull(sum(case when abs(%(target_ref_field)s) > abs(%(target_field)s) then abs(%(target_field)s) else abs(%(target_ref_field)s) end), 0)
|
ifnull(sum(case when abs(%(target_ref_field)s) > abs(%(target_field)s) then abs(%(target_field)s) else abs(%(target_ref_field)s) end), 0)
|
||||||
/ sum(abs(%(target_ref_field)s)) * 100
|
/ sum(abs(%(target_ref_field)s)) * 100
|
||||||
from `tab%(target_dt)s` where parent='%(name)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
|
from `tab%(target_dt)s` where parent='%(name)s' and parenttype='%(target_parent_dt)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
|
||||||
%(update_modified)s
|
%(update_modified)s
|
||||||
where name='%(name)s'"""
|
where name='%(name)s'"""
|
||||||
% args
|
% args
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ class SubcontractingController(StockController):
|
|||||||
"allow_zero_valuation": 1,
|
"allow_zero_valuation": 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
rm_obj.rate = get_incoming_rate(args)
|
rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
|
||||||
|
|
||||||
if self.doctype == self.subcontract_data.order_doctype:
|
if self.doctype == self.subcontract_data.order_doctype:
|
||||||
rm_obj.required_qty = qty
|
rm_obj.required_qty = qty
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ def get_data(filters):
|
|||||||
`tabAddress`.name=`tabDynamic Link`.parent)
|
`tabAddress`.name=`tabDynamic Link`.parent)
|
||||||
WHERE
|
WHERE
|
||||||
company = %(company)s
|
company = %(company)s
|
||||||
AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s
|
AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s
|
||||||
{conditions}
|
{conditions}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
`tabLead`.creation asc """.format(
|
`tabLead`.creation asc """.format(
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ def get_data(filters):
|
|||||||
{join}
|
{join}
|
||||||
WHERE
|
WHERE
|
||||||
`tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s
|
`tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s
|
||||||
AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s
|
AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s
|
||||||
{conditions}
|
{conditions}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
`tabOpportunity`.name
|
`tabOpportunity`.name
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
|
|||||||
if e.code == "ITEM_LOGIN_REQUIRED":
|
if e.code == "ITEM_LOGIN_REQUIRED":
|
||||||
msg = _("There was an error syncing transactions.") + " "
|
msg = _("There was an error syncing transactions.") + " "
|
||||||
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
|
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
|
||||||
frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
|
frappe.log_error(message=msg, title=_("Plaid Link Refresh Required"))
|
||||||
|
|
||||||
return transactions
|
return transactions
|
||||||
|
|
||||||
|
|||||||
@@ -943,7 +943,8 @@ def get_valuation_rate(data):
|
|||||||
2) If no value, get last valuation rate from SLE
|
2) If no value, get last valuation rate from SLE
|
||||||
3) If no value, get valuation rate from Item
|
3) If no value, get valuation rate from Item
|
||||||
"""
|
"""
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Count, IfNull, Sum
|
||||||
|
from pypika import Case
|
||||||
|
|
||||||
item_code, company = data.get("item_code"), data.get("company")
|
item_code, company = data.get("item_code"), data.get("company")
|
||||||
valuation_rate = 0.0
|
valuation_rate = 0.0
|
||||||
@@ -954,7 +955,14 @@ def get_valuation_rate(data):
|
|||||||
frappe.qb.from_(bin_table)
|
frappe.qb.from_(bin_table)
|
||||||
.join(wh_table)
|
.join(wh_table)
|
||||||
.on(bin_table.warehouse == wh_table.name)
|
.on(bin_table.warehouse == wh_table.name)
|
||||||
.select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate"))
|
.select(
|
||||||
|
Case()
|
||||||
|
.when(
|
||||||
|
Count(bin_table.name) > 0, IfNull(Sum(bin_table.stock_value) / Sum(bin_table.actual_qty), 0.0)
|
||||||
|
)
|
||||||
|
.else_(None)
|
||||||
|
.as_("valuation_rate")
|
||||||
|
)
|
||||||
.where((bin_table.item_code == item_code) & (wh_table.company == company))
|
.where((bin_table.item_code == item_code) & (wh_table.company == company))
|
||||||
).run(as_dict=True)[0]
|
).run(as_dict=True)[0]
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ def queue_bom_cost_jobs(
|
|||||||
|
|
||||||
while current_boms_list:
|
while current_boms_list:
|
||||||
batch_no += 1
|
batch_no += 1
|
||||||
batch_size = 20_000
|
batch_size = 7_000
|
||||||
boms_to_process = current_boms_list[:batch_size] # slice out batch of 20k BOMs
|
boms_to_process = current_boms_list[:batch_size] # slice out batch of 20k BOMs
|
||||||
|
|
||||||
# update list to exclude 20K (queued) BOMs
|
# update list to exclude 20K (queued) BOMs
|
||||||
@@ -212,7 +212,7 @@ def resume_bom_cost_update_jobs():
|
|||||||
["name", "boms_updated", "status"],
|
["name", "boms_updated", "status"],
|
||||||
)
|
)
|
||||||
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
|
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
|
||||||
if not bom_batches or not incomplete_level:
|
if not bom_batches or incomplete_level:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Prep parent BOMs & updated processed BOMs for next level
|
# Prep parent BOMs & updated processed BOMs for next level
|
||||||
@@ -252,9 +252,6 @@ def get_processed_current_boms(
|
|||||||
current_boms = []
|
current_boms = []
|
||||||
|
|
||||||
for row in bom_batches:
|
for row in bom_batches:
|
||||||
if not row.boms_updated:
|
|
||||||
continue
|
|
||||||
|
|
||||||
boms_updated = json.loads(row.boms_updated)
|
boms_updated = json.loads(row.boms_updated)
|
||||||
current_boms.extend(boms_updated)
|
current_boms.extend(boms_updated)
|
||||||
boms_updated_dict = {bom: True for bom in boms_updated}
|
boms_updated_dict = {bom: True for bom in boms_updated}
|
||||||
|
|||||||
@@ -344,6 +344,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "prod_plan_references",
|
"fieldname": "prod_plan_references",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
|
"hidden": 1,
|
||||||
"label": "Production Plan Item Reference",
|
"label": "Production Plan Item Reference",
|
||||||
"options": "Production Plan Item Reference"
|
"options": "Production Plan Item Reference"
|
||||||
},
|
},
|
||||||
@@ -397,7 +398,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-26 14:51:08.774372",
|
"modified": "2023-03-31 10:30:48.118932",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan",
|
"name": "Production Plan",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"fieldname": "qty",
|
"fieldname": "qty",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "qty"
|
"label": "Qty"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "item_reference",
|
"fieldname": "item_reference",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-07 17:03:49.707487",
|
"modified": "2023-03-31 10:30:14.604051",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Item Reference",
|
"name": "Production Plan Item Reference",
|
||||||
@@ -48,5 +48,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// allow for '0' qty on Credit/Debit notes
|
// allow for '0' qty on Credit/Debit notes
|
||||||
let qty = item.qty || me.frm.doc.is_debit_note ? 1 : -1;
|
let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1);
|
||||||
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
|
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1897,20 +1897,60 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
make_payment_entry() {
|
make_payment_entry() {
|
||||||
|
let via_journal_entry = this.frm.doc.__onload && this.frm.doc.__onload.make_payment_via_journal_entry;
|
||||||
|
if(this.has_discount_in_schedule() && !via_journal_entry) {
|
||||||
|
// If early payment discount is applied, ask user for reference date
|
||||||
|
this.prompt_user_for_reference_date();
|
||||||
|
} else {
|
||||||
|
this.make_mapped_payment_entry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_mapped_payment_entry(args) {
|
||||||
|
var me = this;
|
||||||
|
args = args || { "dt": this.frm.doc.doctype, "dn": this.frm.doc.name };
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: cur_frm.cscript.get_method_for_payment(),
|
method: me.get_method_for_payment(),
|
||||||
args: {
|
args: args,
|
||||||
"dt": cur_frm.doc.doctype,
|
|
||||||
"dn": cur_frm.doc.name
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
var doclist = frappe.model.sync(r.message);
|
var doclist = frappe.model.sync(r.message);
|
||||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||||
// cur_frm.refresh_fields()
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prompt_user_for_reference_date(){
|
||||||
|
var me = this;
|
||||||
|
frappe.prompt({
|
||||||
|
label: __("Cheque/Reference Date"),
|
||||||
|
fieldname: "reference_date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
reqd: 1,
|
||||||
|
}, (values) => {
|
||||||
|
let args = {
|
||||||
|
"dt": me.frm.doc.doctype,
|
||||||
|
"dn": me.frm.doc.name,
|
||||||
|
"reference_date": values.reference_date
|
||||||
|
}
|
||||||
|
me.make_mapped_payment_entry(args);
|
||||||
|
},
|
||||||
|
__("Reference Date for Early Payment Discount"),
|
||||||
|
__("Continue")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
has_discount_in_schedule() {
|
||||||
|
let is_eligible = in_list(
|
||||||
|
["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"],
|
||||||
|
this.frm.doctype
|
||||||
|
);
|
||||||
|
let has_payment_schedule = this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length;
|
||||||
|
if(!is_eligible || !has_payment_schedule) return false;
|
||||||
|
|
||||||
|
let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date);
|
||||||
|
return has_discount;
|
||||||
|
}
|
||||||
|
|
||||||
make_quality_inspection() {
|
make_quality_inspection() {
|
||||||
let data = [];
|
let data = [];
|
||||||
const fields = [
|
const fields = [
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ frappe.ui.form.on("Customer", {
|
|||||||
|
|
||||||
frm.add_custom_button(__('Accounting Ledger'), function () {
|
frm.add_custom_button(__('Accounting Ledger'), function () {
|
||||||
frappe.set_route('query-report', 'General Ledger',
|
frappe.set_route('query-report', 'General Ledger',
|
||||||
{party_type: 'Customer', party: frm.doc.name});
|
{party_type: 'Customer', party: frm.doc.name, party_name: frm.doc.customer_name});
|
||||||
}, __('View'));
|
}, __('View'));
|
||||||
|
|
||||||
frm.add_custom_button(__('Pricing Rule'), function () {
|
frm.add_custom_button(__('Pricing Rule'), function () {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.naming import make_autoname, revert_series_if_last
|
from frappe.model.naming import make_autoname, revert_series_if_last
|
||||||
from frappe.utils import cint, flt, get_link_to_form
|
from frappe.utils import cint, flt, get_link_to_form, nowtime
|
||||||
from frappe.utils.data import add_days
|
from frappe.utils.data import add_days
|
||||||
from frappe.utils.jinja import render_template
|
from frappe.utils.jinja import render_template
|
||||||
|
|
||||||
@@ -179,7 +179,11 @@ def get_batch_qty(
|
|||||||
out = 0
|
out = 0
|
||||||
if batch_no and warehouse:
|
if batch_no and warehouse:
|
||||||
cond = ""
|
cond = ""
|
||||||
if posting_date and posting_time:
|
|
||||||
|
if posting_date:
|
||||||
|
if posting_time is None:
|
||||||
|
posting_time = nowtime()
|
||||||
|
|
||||||
cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format(
|
cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format(
|
||||||
posting_date, posting_time
|
posting_date, posting_time
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint
|
from frappe import _, bold, msgprint
|
||||||
from frappe.utils import cint, cstr, flt
|
from frappe.utils import cint, cstr, flt
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -89,7 +89,7 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
if item_dict.get("serial_nos"):
|
if item_dict.get("serial_nos"):
|
||||||
item.current_serial_no = item_dict.get("serial_nos")
|
item.current_serial_no = item_dict.get("serial_nos")
|
||||||
if self.purpose == "Stock Reconciliation" and not item.serial_no:
|
if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty:
|
||||||
item.serial_no = item.current_serial_no
|
item.serial_no = item.current_serial_no
|
||||||
|
|
||||||
item.current_qty = item_dict.get("qty")
|
item.current_qty = item_dict.get("qty")
|
||||||
@@ -140,6 +140,14 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
self.validate_item(row.item_code, row)
|
self.validate_item(row.item_code, row)
|
||||||
|
|
||||||
|
if row.serial_no and not row.qty:
|
||||||
|
self.validation_messages.append(
|
||||||
|
_get_msg(
|
||||||
|
row_num,
|
||||||
|
f"Quantity should not be zero for the {bold(row.item_code)} since serial nos are specified",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# validate warehouse
|
# validate warehouse
|
||||||
if not frappe.db.get_value("Warehouse", row.warehouse):
|
if not frappe.db.get_value("Warehouse", row.warehouse):
|
||||||
self.validation_messages.append(_get_msg(row_num, _("Warehouse not found in the system")))
|
self.validation_messages.append(_get_msg(row_num, _("Warehouse not found in the system")))
|
||||||
|
|||||||
@@ -35,7 +35,14 @@ purchase_doctypes = [
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True):
|
def get_item_details(
|
||||||
|
args,
|
||||||
|
doc=None,
|
||||||
|
for_validate=False,
|
||||||
|
overwrite_warehouse=True,
|
||||||
|
return_basic_details=False,
|
||||||
|
basic_details=None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
args = {
|
args = {
|
||||||
"item_code": "",
|
"item_code": "",
|
||||||
@@ -73,7 +80,13 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
|||||||
if doc.get("doctype") == "Purchase Invoice":
|
if doc.get("doctype") == "Purchase Invoice":
|
||||||
args["bill_date"] = doc.get("bill_date")
|
args["bill_date"] = doc.get("bill_date")
|
||||||
|
|
||||||
out = get_basic_details(args, item, overwrite_warehouse)
|
if not basic_details:
|
||||||
|
out = get_basic_details(args, item, overwrite_warehouse)
|
||||||
|
else:
|
||||||
|
out = basic_details
|
||||||
|
|
||||||
|
basic_details = out.copy()
|
||||||
|
|
||||||
get_item_tax_template(args, item, out)
|
get_item_tax_template(args, item, out)
|
||||||
out["item_tax_rate"] = get_item_tax_map(
|
out["item_tax_rate"] = get_item_tax_map(
|
||||||
args.company,
|
args.company,
|
||||||
@@ -141,7 +154,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
|||||||
out.amount = flt(args.qty) * flt(out.rate)
|
out.amount = flt(args.qty) * flt(out.rate)
|
||||||
|
|
||||||
out = remove_standard_fields(out)
|
out = remove_standard_fields(out)
|
||||||
return out
|
|
||||||
|
if return_basic_details:
|
||||||
|
return out, basic_details
|
||||||
|
else:
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def remove_standard_fields(details):
|
def remove_standard_fields(details):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, TypedDict
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import CombineDatetime
|
from frappe.query_builder.functions import Coalesce, CombineDatetime
|
||||||
from frappe.utils import cint, date_diff, flt, getdate
|
from frappe.utils import cint, date_diff, flt, getdate
|
||||||
from frappe.utils.nestedset import get_descendants_of
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
@@ -322,6 +322,34 @@ def get_stock_ledger_entries(filters: StockBalanceFilter, items: List[str]) -> L
|
|||||||
return query.run(as_dict=True)
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_opening_vouchers(to_date):
|
||||||
|
opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []}
|
||||||
|
|
||||||
|
se = frappe.qb.DocType("Stock Entry")
|
||||||
|
sr = frappe.qb.DocType("Stock Reconciliation")
|
||||||
|
|
||||||
|
vouchers_data = (
|
||||||
|
frappe.qb.from_(
|
||||||
|
(
|
||||||
|
frappe.qb.from_(se)
|
||||||
|
.select(se.name, Coalesce("Stock Entry").as_("voucher_type"))
|
||||||
|
.where((se.docstatus == 1) & (se.posting_date <= to_date) & (se.is_opening == "Yes"))
|
||||||
|
)
|
||||||
|
+ (
|
||||||
|
frappe.qb.from_(sr)
|
||||||
|
.select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type"))
|
||||||
|
.where((sr.docstatus == 1) & (sr.posting_date <= to_date) & (sr.purpose == "Opening Stock"))
|
||||||
|
)
|
||||||
|
).select("voucher_type", "name")
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
if vouchers_data:
|
||||||
|
for d in vouchers_data:
|
||||||
|
opening_vouchers[d.voucher_type].append(d.name)
|
||||||
|
|
||||||
|
return opening_vouchers
|
||||||
|
|
||||||
|
|
||||||
def get_inventory_dimension_fields():
|
def get_inventory_dimension_fields():
|
||||||
return [dimension.fieldname for dimension in get_inventory_dimensions()]
|
return [dimension.fieldname for dimension in get_inventory_dimensions()]
|
||||||
|
|
||||||
@@ -330,9 +358,8 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]):
|
|||||||
iwb_map = {}
|
iwb_map = {}
|
||||||
from_date = getdate(filters.get("from_date"))
|
from_date = getdate(filters.get("from_date"))
|
||||||
to_date = getdate(filters.get("to_date"))
|
to_date = getdate(filters.get("to_date"))
|
||||||
|
opening_vouchers = get_opening_vouchers(to_date)
|
||||||
float_precision = cint(frappe.db.get_default("float_precision")) or 3
|
float_precision = cint(frappe.db.get_default("float_precision")) or 3
|
||||||
|
|
||||||
inventory_dimensions = get_inventory_dimension_fields()
|
inventory_dimensions = get_inventory_dimension_fields()
|
||||||
|
|
||||||
for d in sle:
|
for d in sle:
|
||||||
@@ -363,11 +390,7 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]):
|
|||||||
|
|
||||||
value_diff = flt(d.stock_value_difference)
|
value_diff = flt(d.stock_value_difference)
|
||||||
|
|
||||||
if d.posting_date < from_date or (
|
if d.posting_date < from_date or d.voucher_no in opening_vouchers.get(d.voucher_type, []):
|
||||||
d.posting_date == from_date
|
|
||||||
and d.voucher_type == "Stock Reconciliation"
|
|
||||||
and frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock"
|
|
||||||
):
|
|
||||||
qty_dict.opening_qty += qty_diff
|
qty_dict.opening_qty += qty_diff
|
||||||
qty_dict.opening_val += value_diff
|
qty_dict.opening_val += value_diff
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ def execute(filters=None):
|
|||||||
conversion_factors.append(0)
|
conversion_factors.append(0)
|
||||||
|
|
||||||
actual_qty = stock_value = 0
|
actual_qty = stock_value = 0
|
||||||
|
if opening_row:
|
||||||
|
actual_qty = opening_row.get("qty_after_transaction")
|
||||||
|
stock_value = opening_row.get("stock_value")
|
||||||
|
|
||||||
available_serial_nos = {}
|
available_serial_nos = {}
|
||||||
inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters)
|
inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters)
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ class TransactionBase(StatusUpdater):
|
|||||||
|
|
||||||
def compare_values(self, ref_doc, fields, doc=None):
|
def compare_values(self, ref_doc, fields, doc=None):
|
||||||
for reference_doctype, ref_dn_list in ref_doc.items():
|
for reference_doctype, ref_dn_list in ref_doc.items():
|
||||||
|
prev_doc_detail_map = self.get_prev_doc_reference_details(
|
||||||
|
ref_dn_list, reference_doctype, fields
|
||||||
|
)
|
||||||
for reference_name in ref_dn_list:
|
for reference_name in ref_dn_list:
|
||||||
prevdoc_values = frappe.db.get_value(
|
prevdoc_values = prev_doc_detail_map.get(reference_name)
|
||||||
reference_doctype, reference_name, [d[0] for d in fields], as_dict=1
|
|
||||||
)
|
|
||||||
|
|
||||||
if not prevdoc_values:
|
if not prevdoc_values:
|
||||||
frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name))
|
frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name))
|
||||||
|
|
||||||
@@ -70,6 +70,19 @@ class TransactionBase(StatusUpdater):
|
|||||||
if prevdoc_values[field] is not None and field not in self.exclude_fields:
|
if prevdoc_values[field] is not None and field not in self.exclude_fields:
|
||||||
self.validate_value(field, condition, prevdoc_values[field], doc)
|
self.validate_value(field, condition, prevdoc_values[field], doc)
|
||||||
|
|
||||||
|
def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields):
|
||||||
|
prev_doc_detail_map = {}
|
||||||
|
details = frappe.get_all(
|
||||||
|
reference_doctype,
|
||||||
|
filters={"name": ("in", reference_names)},
|
||||||
|
fields=["name"] + [d[0] for d in fields],
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in details:
|
||||||
|
prev_doc_detail_map.setdefault(d.name, d)
|
||||||
|
|
||||||
|
return prev_doc_detail_map
|
||||||
|
|
||||||
def validate_rate_with_reference_doc(self, ref_details):
|
def validate_rate_with_reference_doc(self, ref_details):
|
||||||
if self.get("is_internal_supplier"):
|
if self.get("is_internal_supplier"):
|
||||||
return
|
return
|
||||||
@@ -77,23 +90,23 @@ class TransactionBase(StatusUpdater):
|
|||||||
buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
|
buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
|
||||||
|
|
||||||
if self.doctype in buying_doctypes:
|
if self.doctype in buying_doctypes:
|
||||||
action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action")
|
action, role_allowed_to_override = frappe.get_cached_value(
|
||||||
settings_doc = "Buying Settings"
|
"Buying Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
action = frappe.db.get_single_value("Selling Settings", "maintain_same_rate_action")
|
action, role_allowed_to_override = frappe.get_cached_value(
|
||||||
settings_doc = "Selling Settings"
|
"Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
|
||||||
|
)
|
||||||
|
|
||||||
for ref_dt, ref_dn_field, ref_link_field in ref_details:
|
for ref_dt, ref_dn_field, ref_link_field in ref_details:
|
||||||
|
reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
|
||||||
|
reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.get(ref_link_field):
|
if d.get(ref_link_field):
|
||||||
ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate")
|
ref_rate = reference_details.get(d.get(ref_link_field))
|
||||||
|
|
||||||
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
|
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
|
||||||
if action == "Stop":
|
if action == "Stop":
|
||||||
role_allowed_to_override = frappe.db.get_single_value(
|
|
||||||
settings_doc, "role_to_override_stop_action"
|
|
||||||
)
|
|
||||||
|
|
||||||
if role_allowed_to_override not in frappe.get_roles():
|
if role_allowed_to_override not in frappe.get_roles():
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
|
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
|
||||||
@@ -109,6 +122,16 @@ class TransactionBase(StatusUpdater):
|
|||||||
indicator="orange",
|
indicator="orange",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_reference_details(self, reference_names, reference_doctype):
|
||||||
|
return frappe._dict(
|
||||||
|
frappe.get_all(
|
||||||
|
reference_doctype,
|
||||||
|
filters={"name": ("in", reference_names)},
|
||||||
|
fields=["name", "rate"],
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_link_filters(self, for_doctype):
|
def get_link_filters(self, for_doctype):
|
||||||
if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype):
|
if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype):
|
||||||
fieldname = self.prev_link_mapper[for_doctype]["fieldname"]
|
fieldname = self.prev_link_mapper[for_doctype]["fieldname"]
|
||||||
@@ -186,12 +209,15 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
|
|||||||
for f in qty_fields:
|
for f in qty_fields:
|
||||||
qty = d.get(f)
|
qty = d.get(f)
|
||||||
if qty:
|
if qty:
|
||||||
if abs(cint(qty) - flt(qty)) > 0.0000001:
|
if abs(cint(qty) - flt(qty, d.precision(f))) > 0.0000001:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}."
|
"Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}."
|
||||||
).format(
|
).format(
|
||||||
qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field))
|
flt(qty, d.precision(f)),
|
||||||
|
d.idx,
|
||||||
|
frappe.bold(_("Must be Whole Number")),
|
||||||
|
frappe.bold(d.get(uom_field)),
|
||||||
),
|
),
|
||||||
UOMMustBeIntegerError,
|
UOMMustBeIntegerError,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user