Merge branch 'develop' into stock-reservation

This commit is contained in:
s-aga-r 2023-04-11 10:00:39 +05:30
commit a14a6002e7
81 changed files with 1652 additions and 1082 deletions

View File

@ -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",
@ -360,6 +361,13 @@
"fieldname": "show_balance_in_coa", "fieldname": "show_balance_in_coa",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Balances in Chart Of Accounts" "label": "Show Balances in Chart Of Accounts"
},
{
"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",
@ -367,7 +375,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-01-02 12:07:42.434214", "modified": "2023-03-28 09:50:20.375233",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

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

View File

@ -46,7 +46,7 @@ class BankTransaction(StatusUpdater):
def add_payment_entries(self, vouchers): def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance" "Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
if 0.0 >= self.unallocated_amount: if 0.0 >= self.unallocated_amount:
frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled")) frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
added = False added = False
for voucher in vouchers: for voucher in vouchers:
@ -114,9 +114,7 @@ class BankTransaction(StatusUpdater):
elif 0.0 > unallocated_amount: elif 0.0 > unallocated_amount:
self.db_delete_payment_entry(payment_entry) self.db_delete_payment_entry(payment_entry)
frappe.throw( frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
)
self.reload() self.reload()
@ -178,7 +176,9 @@ def get_clearance_details(transaction, payment_entry):
if gle["gl_account"] == gl_bank_account: if gle["gl_account"] == gl_bank_account:
if gle["amount"] <= 0.0: if gle["amount"] <= 0.0:
frappe.throw( frappe.throw(
frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}") frappe._("Voucher {0} value is broken: {1}").format(
payment_entry.payment_entry, gle["amount"]
)
) )
unmatched_gles -= 1 unmatched_gles -= 1

View File

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

View File

@ -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:

View File

@ -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()
@ -666,7 +666,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(

View File

@ -244,8 +244,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));

View File

@ -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(

View File

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

View File

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

View File

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

View File

@ -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()

View File

@ -497,10 +497,16 @@ def get_amount(ref_doc, payment_account=None):
if dt in ["Sales Order", "Purchase Order"]: if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
elif dt in ["Sales Invoice", "Purchase Invoice"]: elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency: if not ref_doc.get("is_pos"):
grand_total = flt(ref_doc.outstanding_amount) if ref_doc.party_account_currency == ref_doc.currency:
else: grand_total = flt(ref_doc.outstanding_amount)
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
elif dt == "Sales Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount
break
elif dt == "POS Invoice": elif dt == "POS Invoice":
for pay in ref_doc.payments: for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account: if pay.type == "Phone" and pay.account == payment_account:

View File

@ -6,6 +6,7 @@ import unittest
import frappe import frappe
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
@ -74,6 +75,29 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.reference_name, si_usd.name)
self.assertEqual(pr.currency, "USD") self.assertEqual(pr.currency, "USD")
def test_payment_entry_against_purchase_invoice(self):
si_usd = make_purchase_invoice(
customer="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
currency="USD",
conversion_rate=50,
)
pr = make_payment_request(
dt="Purchase Invoice",
dn=si_usd.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
submit_doc=1,
return_doc=1,
)
pe = pr.create_payment_entry()
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
def test_payment_entry(self): def test_payment_entry(self):
frappe.db.set_value( frappe.db.set_value(
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"

View File

@ -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'));
} }

View File

@ -118,6 +118,7 @@
"paid_amount", "paid_amount",
"advances_section", "advances_section",
"allocate_advances_automatically", "allocate_advances_automatically",
"only_include_allocated_payments",
"get_advances", "get_advances",
"advances", "advances",
"advance_tax", "advance_tax",
@ -1550,17 +1551,24 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "label": "Named Place"
},
{
"default": "0",
"depends_on": "allocate_advances_automatically",
"description": "Advance payments allocated against orders will only be fetched",
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-28 19:18:56.586321", "modified": "2023-04-03 22:57:14.074982",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@ -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")

View File

@ -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) {

View File

@ -120,6 +120,7 @@
"account_for_change_amount", "account_for_change_amount",
"advances_section", "advances_section",
"allocate_advances_automatically", "allocate_advances_automatically",
"only_include_allocated_payments",
"get_advances", "get_advances",
"advances", "advances",
"write_off_section", "write_off_section",
@ -2126,6 +2127,14 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "label": "Named Place"
},
{
"default": "0",
"depends_on": "allocate_advances_automatically",
"description": "Advance payments allocated against orders will only be fetched",
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -2138,7 +2147,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-03-13 11:43:15.883055", "modified": "2023-04-03 22:55:14.206473",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -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:

View File

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

View File

@ -25,6 +25,7 @@ def get_data(filters):
["posting_date", "<=", filters.get("to_date")], ["posting_date", "<=", filters.get("to_date")],
["against_voucher_type", "=", "Asset"], ["against_voucher_type", "=", "Asset"],
["account", "in", depreciation_accounts], ["account", "in", depreciation_accounts],
["is_cancelled", "=", 0],
] ]
if filters.get("asset"): if filters.get("asset"):

View File

@ -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()),
)

View File

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

View File

@ -467,14 +467,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(
_("Validation Error for {0}").format(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)

View File

@ -466,6 +466,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)

View File

@ -79,6 +79,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,
@ -517,7 +520,7 @@
"table_fieldname": "accounts" "table_fieldname": "accounts"
} }
], ],
"modified": "2023-02-02 00:03:11.706427", "modified": "2023-03-30 15:07:41.542374",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -8,16 +8,12 @@ import math
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import ( from frappe.utils import (
add_months,
cint, cint,
date_diff,
flt, flt,
get_datetime, get_datetime,
get_last_day, get_last_day,
get_link_to_form, get_link_to_form,
getdate, getdate,
is_last_day_of_the_month,
month_diff,
nowdate, nowdate,
today, today,
) )
@ -239,30 +235,6 @@ class Asset(AccountsController):
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
) )
# if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
has_pro_rata = False
# if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
from_date = self.get_modified_available_for_use_date(row)
days = date_diff(row.depreciation_start_date, from_date) + 1
# if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
has_pro_rata = True
return has_pro_rata
def get_modified_available_for_use_date(self, row):
return add_months(
self.available_for_use_date,
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
)
def validate_asset_finance_books(self, row): def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw( frappe.throw(
@ -471,29 +443,6 @@ class Asset(AccountsController):
return records return records
@erpnext.allow_regional
def get_depreciation_amount(self, depreciable_value, fb_row):
if fb_row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if self.flags.increase_in_asset_life:
depreciation_amount = (
flt(fb_row.value_after_depreciation) - flt(fb_row.expected_value_after_useful_life)
) / (date_diff(self.to_date, self.available_for_use_date) / 365)
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
elif self.flags.increase_in_asset_value_due_to_repair:
depreciation_amount = (
flt(fb_row.value_after_depreciation) - flt(fb_row.expected_value_after_useful_life)
) / flt(fb_row.total_number_of_depreciations)
# if the Depreciation Schedule is being prepared for the first time
else:
depreciation_amount = (
flt(self.gross_purchase_amount) - flt(fb_row.expected_value_after_useful_life)
) / flt(fb_row.total_number_of_depreciations)
else:
depreciation_amount = flt(depreciable_value * (flt(fb_row.rate_of_depreciation) / 100))
return depreciation_amount
def validate_make_gl_entry(self): def validate_make_gl_entry(self):
purchase_document = self.get_purchase_document() purchase_document = self.get_purchase_document()
if not purchase_document: if not purchase_document:
@ -618,7 +567,12 @@ class Asset(AccountsController):
float_precision = cint(frappe.db.get_default("float_precision")) or 2 float_precision = cint(frappe.db.get_default("float_precision")) or 2
if args.get("depreciation_method") == "Double Declining Balance": if args.get("depreciation_method") == "Double Declining Balance":
return 200.0 / args.get("total_number_of_depreciations") return 200.0 / (
(
flt(args.get("total_number_of_depreciations"), 2) * flt(args.get("frequency_of_depreciation"))
)
/ 12
)
if args.get("depreciation_method") == "Written Down Value": if args.get("depreciation_method") == "Written Down Value":
if ( if (
@ -635,17 +589,20 @@ class Asset(AccountsController):
else: else:
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount) value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2)) depreciation_rate = math.pow(
value,
1.0
/ (
(
flt(args.get("total_number_of_depreciations"), 2)
* flt(args.get("frequency_of_depreciation"))
)
/ 12
),
)
return flt((100 * (1 - depreciation_rate)), float_precision) return flt((100 * (1 - depreciation_rate)), float_precision)
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
def update_maintenance_status(): def update_maintenance_status():
assets = frappe.get_all( assets = frappe.get_all(
@ -889,15 +846,6 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
return asset.get_value_after_depreciation(finance_book) return asset.get_value_after_depreciation(finance_book)
def get_total_days(date, frequency):
period_start_date = add_months(date, cint(frequency) * -1)
if is_last_day_of_the_month(date):
period_start_date = get_last_day(period_start_date)
return date_diff(date, period_start_date)
@frappe.whitelist() @frappe.whitelist()
def split_asset(asset_name, split_qty): def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)

View File

@ -36,7 +36,7 @@ frappe.listview_settings['Asset'] = {
} }
}, },
onload: function(me) { onload: function(me) {
me.page.add_action_item('Make Asset Movement', function() { me.page.add_action_item(__("Make Asset Movement"), function() {
const assets = me.get_checked_items(); const assets = me.get_checked_items();
frappe.call({ frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_asset_movement", method: "erpnext.assets.doctype.asset.asset.make_asset_movement",

View File

@ -249,10 +249,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)

View File

@ -29,8 +29,11 @@ from erpnext.assets.doctype.asset.depreciation import (
scrap_asset, scrap_asset,
) )
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
_check_is_pro_rata,
_get_pro_rata_amt,
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_depr_schedule, get_depr_schedule,
get_depreciation_amount,
) )
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_invoice, make_purchase_invoice as make_invoice,
@ -234,7 +237,7 @@ class TestAsset(AssetSetup):
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"), asset.precision("gross_purchase_amount"),
) )
pro_rata_amount, _, _ = asset.get_pro_rata_amt( pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
) )
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
@ -321,7 +324,7 @@ class TestAsset(AssetSetup):
self.assertEquals(second_asset_depr_schedule.status, "Active") self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled") self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
pro_rata_amount, _, _ = asset.get_pro_rata_amt( pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
) )
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
@ -857,12 +860,12 @@ class TestDepreciationMethods(AssetSetup):
) )
expected_schedules = [ expected_schedules = [
["2022-02-28", 647.25, 647.25], ["2022-02-28", 310.89, 310.89],
["2022-03-31", 1210.71, 1857.96], ["2022-03-31", 654.45, 965.34],
["2022-04-30", 1053.99, 2911.95], ["2022-04-30", 654.45, 1619.79],
["2022-05-31", 917.55, 3829.5], ["2022-05-31", 654.45, 2274.24],
["2022-06-30", 798.77, 4628.27], ["2022-06-30", 654.45, 2928.69],
["2022-07-15", 371.73, 5000.0], ["2022-07-15", 2071.31, 5000.0],
] ]
schedules = [ schedules = [
@ -938,7 +941,7 @@ class TestDepreciationBasics(AssetSetup):
}, },
) )
depreciation_amount = asset.get_depreciation_amount(100000, asset.finance_books[0]) depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0])
self.assertEqual(depreciation_amount, 30000) self.assertEqual(depreciation_amount, 30000)
def test_make_depr_schedule(self): def test_make_depr_schedule(self):
@ -997,7 +1000,7 @@ class TestDepreciationBasics(AssetSetup):
}, },
) )
has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0]) has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0])
self.assertFalse(has_pro_rata) self.assertFalse(has_pro_rata)
asset.finance_books = [] asset.finance_books = []
@ -1012,7 +1015,7 @@ class TestDepreciationBasics(AssetSetup):
}, },
) )
has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0]) has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0])
self.assertTrue(has_pro_rata) self.assertTrue(has_pro_rata)
def test_expected_value_after_useful_life_greater_than_purchase_amount(self): def test_expected_value_after_useful_life_greater_than_purchase_amount(self):

View File

@ -8,12 +8,16 @@ from frappe.utils import (
add_days, add_days,
add_months, add_months,
cint, cint,
date_diff,
flt, flt,
get_last_day, get_last_day,
getdate, getdate,
is_last_day_of_the_month, is_last_day_of_the_month,
month_diff,
) )
import erpnext
class AssetDepreciationSchedule(Document): class AssetDepreciationSchedule(Document):
def before_save(self): def before_save(self):
@ -185,7 +189,7 @@ class AssetDepreciationSchedule(Document):
): ):
asset_doc.validate_asset_finance_books(row) asset_doc.validate_asset_finance_books(row)
value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(asset_doc, row) value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
row.value_after_depreciation = value_after_depreciation row.value_after_depreciation = value_after_depreciation
if update_asset_finance_book_row: if update_asset_finance_book_row:
@ -195,21 +199,46 @@ class AssetDepreciationSchedule(Document):
self.number_of_depreciations_booked self.number_of_depreciations_booked
) )
has_pro_rata = asset_doc.check_is_pro_rata(row) has_pro_rata = _check_is_pro_rata(asset_doc, row)
if has_pro_rata: if has_pro_rata:
number_of_pending_depreciations += 1 number_of_pending_depreciations += 1
has_wdv_or_dd_non_yearly_pro_rata = False
if (
row.depreciation_method in ("Written Down Value", "Double Declining Balance")
and cint(row.frequency_of_depreciation) != 12
):
has_wdv_or_dd_non_yearly_pro_rata = _check_is_pro_rata(
asset_doc, row, wdv_or_dd_non_yearly=True
)
skip_row = False skip_row = False
should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date) should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date)
depreciation_amount = 0
for n in range(start, number_of_pending_depreciations): for n in range(start, number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance) # If depreciation is already completed (for double declining balance)
if skip_row: if skip_row:
continue continue
depreciation_amount = asset_doc.get_depreciation_amount(value_after_depreciation, row) if n > 0 and len(self.get("depreciation_schedule")) > n - 1:
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
else:
prev_depreciation_amount = 0
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: depreciation_amount = get_depreciation_amount(
asset_doc,
value_after_depreciation,
row,
n,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
)
if not has_pro_rata or (
n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
):
schedule_date = add_months( schedule_date = add_months(
row.depreciation_start_date, n * cint(row.frequency_of_depreciation) row.depreciation_start_date, n * cint(row.frequency_of_depreciation)
) )
@ -227,8 +256,11 @@ class AssetDepreciationSchedule(Document):
if self.depreciation_schedule: if self.depreciation_schedule:
from_date = self.depreciation_schedule[-1].schedule_date from_date = self.depreciation_schedule[-1].schedule_date
depreciation_amount, days, months = asset_doc.get_pro_rata_amt( depreciation_amount, days, months = _get_pro_rata_amt(
row, depreciation_amount, from_date, date_of_disposal row,
depreciation_amount,
from_date,
date_of_disposal,
) )
if depreciation_amount > 0: if depreciation_amount > 0:
@ -240,12 +272,20 @@ class AssetDepreciationSchedule(Document):
break break
# For first row # For first row
if has_pro_rata and not self.opening_accumulated_depreciation and n == 0: if (
(has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation
and n == 0
):
from_date = add_days( from_date = add_days(
asset_doc.available_for_use_date, -1 asset_doc.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too ) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = asset_doc.get_pro_rata_amt( depreciation_amount, days, months = _get_pro_rata_amt(
row, depreciation_amount, from_date, row.depreciation_start_date row,
depreciation_amount,
from_date,
row.depreciation_start_date,
has_wdv_or_dd_non_yearly_pro_rata,
) )
# For first depr schedule date will be the start date # For first depr schedule date will be the start date
@ -264,8 +304,12 @@ class AssetDepreciationSchedule(Document):
depreciation_amount_without_pro_rata = depreciation_amount depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = asset_doc.get_pro_rata_amt( depreciation_amount, days, months = _get_pro_rata_amt(
row, depreciation_amount, schedule_date, asset_doc.to_date row,
depreciation_amount,
schedule_date,
asset_doc.to_date,
has_wdv_or_dd_non_yearly_pro_rata,
) )
depreciation_amount = self.get_adjusted_depreciation_amount( depreciation_amount = self.get_adjusted_depreciation_amount(
@ -373,15 +417,142 @@ class AssetDepreciationSchedule(Document):
accumulated_depreciation, d.precision("accumulated_depreciation_amount") accumulated_depreciation, d.precision("accumulated_depreciation_amount")
) )
def _get_value_after_depreciation_for_making_schedule(self, asset_doc, fb_row):
if asset_doc.docstatus == 1 and fb_row.value_after_depreciation:
value_after_depreciation = flt(fb_row.value_after_depreciation)
else:
value_after_depreciation = flt(self.gross_purchase_amount) - flt(
self.opening_accumulated_depreciation
)
return value_after_depreciation def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row):
if asset_doc.docstatus == 1 and fb_row.value_after_depreciation:
value_after_depreciation = flt(fb_row.value_after_depreciation)
else:
value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt(
asset_doc.opening_accumulated_depreciation
)
return value_after_depreciation
# if it returns True, depreciation_amount will not be equal for the first and last rows
def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
has_pro_rata = False
# if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly)
days = date_diff(row.depreciation_start_date, from_date) + 1
if wdv_or_dd_non_yearly:
total_days = get_total_days(row.depreciation_start_date, 12)
else:
# if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
has_pro_rata = True
return has_pro_rata
def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False):
if wdv_or_dd_non_yearly:
return add_months(
asset_doc.available_for_use_date,
(asset_doc.number_of_depreciations_booked * 12),
)
else:
return add_months(
asset_doc.available_for_use_date,
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
)
def _get_pro_rata_amt(
row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False
):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
if has_wdv_or_dd_non_yearly_pro_rata:
total_days = get_total_days(to_date, 12)
else:
total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
def get_total_days(date, frequency):
period_start_date = add_months(date, cint(frequency) * -1)
if is_last_day_of_the_month(date):
period_start_date = get_last_day(period_start_date)
return date_diff(date, period_start_date)
@erpnext.allow_regional
def get_depreciation_amount(
asset,
depreciable_value,
row,
schedule_idx=0,
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
):
if row.depreciation_method in ("Straight Line", "Manual"):
return get_straight_line_or_manual_depr_amount(asset, row)
else:
return get_wdv_or_dd_depr_amount(
depreciable_value,
row.rate_of_depreciation,
row.frequency_of_depreciation,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
)
def get_straight_line_or_manual_depr_amount(asset, row):
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
date_diff(asset.to_date, asset.available_for_use_date) / 365
)
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
elif asset.flags.increase_in_asset_value_due_to_repair:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
# if the Depreciation Schedule is being prepared for the first time
else:
return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
def get_wdv_or_dd_depr_amount(
depreciable_value,
rate_of_depreciation,
frequency_of_depreciation,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
):
if cint(frequency_of_depreciation) == 12:
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
else:
if has_wdv_or_dd_non_yearly_pro_rata:
if schedule_idx == 0:
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1:
return (
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
)
else:
return prev_depreciation_amount
else:
if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0:
return (
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
)
else:
return prev_depreciation_amount
def make_draft_asset_depr_schedules_if_not_present(asset_doc): def make_draft_asset_depr_schedules_if_not_present(asset_doc):

View File

@ -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 (

View File

@ -1,664 +1,156 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2017-10-20 07:10:55.903571",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "document_type": "Document",
"autoname": "", "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2017-10-20 07:10:55.903571", "field_order": [
"custom": 0, "maintenance_task",
"docstatus": 0, "maintenance_type",
"doctype": "DocType", "column_break_2",
"document_type": "Document", "maintenance_status",
"editable_grid": 1, "section_break_2",
"engine": "InnoDB", "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, "fieldname": "maintenance_task",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "in_filter": 1,
"collapsible": 0, "in_list_view": 1,
"columns": 0, "in_standard_filter": 1,
"fieldname": "maintenance_task", "label": "Maintenance Task",
"fieldtype": "Data", "reqd": 1
"hidden": 0, },
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Maintenance Task",
"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": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "maintenance_type",
"allow_on_submit": 0, "fieldtype": "Select",
"bold": 0, "label": "Maintenance Type",
"collapsible": 0, "options": "Preventive Maintenance\nCalibration"
"columns": 0, },
"fieldname": "maintenance_type",
"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",
"length": 0,
"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, "fieldname": "column_break_2",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"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, "fieldname": "maintenance_status",
"allow_on_submit": 0, "fieldtype": "Select",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Maintenance Status",
"columns": 0, "options": "Planned\nOverdue\nCancelled",
"default": "", "reqd": 1
"fieldname": "maintenance_status", },
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Maintenance Status",
"length": 0,
"no_copy": 0,
"options": "Planned\nOverdue\nCancelled",
"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, "fieldname": "section_break_2",
"allow_on_submit": 0, "fieldtype": "Section Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_2",
"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, "default": "Today",
"allow_on_submit": 0, "fieldname": "start_date",
"bold": 0, "fieldtype": "Date",
"collapsible": 0, "label": "Start Date",
"columns": 0, "reqd": 1
"default": "Today", },
"fieldname": "start_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",
"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": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "periodicity",
"allow_on_submit": 0, "fieldtype": "Select",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Periodicity",
"columns": 0, "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly",
"fieldname": "periodicity", "reqd": 1
"fieldtype": "Select", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Periodicity",
"length": 0,
"no_copy": 0,
"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, "fieldname": "column_break_4",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4",
"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, "fieldname": "end_date",
"allow_on_submit": 0, "fieldtype": "Date",
"bold": 0, "label": "End Date"
"collapsible": 0, },
"columns": 0,
"fieldname": "end_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": "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, "fieldname": "certificate_required",
"bold": 0, "fieldtype": "Check",
"collapsible": 0, "label": "Certificate Required",
"columns": 0, "search_index": 1,
"fieldname": "certificate_required", "set_only_once": 1
"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",
"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,
"set_only_once": 1,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_9",
"allow_on_submit": 0, "fieldtype": "Section Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_9",
"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, "fieldname": "assign_to",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Assign To",
"columns": 0, "options": "User"
"fieldname": "assign_to", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Assign To",
"length": 0,
"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, "fieldname": "column_break_10",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_10",
"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, "fieldname": "section_break_10",
"allow_on_submit": 0, "fieldtype": "Section Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_10",
"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, "fieldname": "next_due_date",
"allow_on_submit": 0, "fieldtype": "Date",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Next Due Date"
"columns": 0, },
"fieldname": "next_due_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_standard_filter": 0,
"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, "fieldname": "column_break_14",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_14",
"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, "fieldname": "last_completion_date",
"allow_on_submit": 0, "fieldtype": "Date",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Last Completion Date"
"columns": 0, },
"fieldname": "last_completion_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_standard_filter": 0,
"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, "fieldname": "section_break_7",
"allow_on_submit": 0, "fieldtype": "Section Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"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, "fieldname": "description",
"allow_on_submit": 0, "fieldtype": "Text Editor",
"bold": 0, "label": "Description"
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"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": "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, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2023-03-23 07:03:07.113452",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Assets",
"in_create": 0, "name": "Asset Maintenance Task",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "quick_entry": 1,
"max_attachments": 0, "sort_field": "modified",
"modified": "2018-06-18 16:12:04.330021", "sort_order": "DESC"
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Maintenance Task",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
} }

View File

@ -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) {

View File

@ -14,6 +14,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciatio
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_depreciation_amount,
) )
@ -162,7 +163,7 @@ class AssetValueAdjustment(Document):
depreciation_amount = days * rate_per_day depreciation_amount = days * rate_per_day
from_date = data.schedule_date from_date = data.schedule_date
else: else:
depreciation_amount = asset.get_depreciation_amount(value_after_depreciation, d) depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
if depreciation_amount: if depreciation_amount:
value_after_depreciation -= flt(depreciation_amount) value_after_depreciation -= flt(depreciation_amount)

View File

@ -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) {

View File

@ -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 () {

View File

@ -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:
@ -833,7 +845,9 @@ class AccountsController(TransactionBase):
def set_advances(self): def set_advances(self):
"""Returns list of advances against Account, Party, Reference""" """Returns list of advances against Account, Party, Reference"""
res = self.get_advance_entries() res = self.get_advance_entries(
include_unallocated=not cint(self.get("only_include_allocated_payments"))
)
self.set("advances", []) self.set("advances", [])
advance_allocated = 0 advance_allocated = 0
@ -1232,7 +1246,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 +1259,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 +1319,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

View File

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

View File

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

View File

@ -33,7 +33,6 @@ class Opportunity(TransactionBase, CRMNote):
def after_insert(self): def after_insert(self):
if self.opportunity_from == "Lead": if self.opportunity_from == "Lead":
frappe.get_doc("Lead", self.party_name).set_status(update=True) frappe.get_doc("Lead", self.party_name).set_status(update=True)
self.disable_lead()
link_open_tasks(self.opportunity_from, self.party_name, self) link_open_tasks(self.opportunity_from, self.party_name, self)
link_open_events(self.opportunity_from, self.party_name, self) link_open_events(self.opportunity_from, self.party_name, self)
@ -119,10 +118,6 @@ class Opportunity(TransactionBase, CRMNote):
prospect.flags.ignore_mandatory = True prospect.flags.ignore_mandatory = True
prospect.save() prospect.save()
def disable_lead(self):
if self.opportunity_from == "Lead":
frappe.db.set_value("Lead", self.party_name, {"disabled": 1, "docstatus": 1})
def make_new_lead_if_required(self): def make_new_lead_if_required(self):
"""Set lead against new opportunity""" """Set lead against new opportunity"""
if (not self.get("party_name")) and self.contact_email: if (not self.get("party_name")) and self.contact_email:

View File

@ -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(

View File

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

View File

@ -199,8 +199,14 @@ class TestWebsiteItem(unittest.TestCase):
breadcrumbs = get_parent_item_groups(item.item_group) breadcrumbs = get_parent_item_groups(item.item_group)
settings = frappe.get_cached_doc("E Commerce Settings")
if settings.enable_field_filters:
base_breadcrumb = "Shop by Category"
else:
base_breadcrumb = "All Products"
self.assertEqual(breadcrumbs[0]["name"], "Home") self.assertEqual(breadcrumbs[0]["name"], "Home")
self.assertEqual(breadcrumbs[1]["name"], "All Products") self.assertEqual(breadcrumbs[1]["name"], base_breadcrumb)
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1") self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")

View File

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

View File

@ -28,6 +28,10 @@ doctype_js = {
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"} override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
override_whitelisted_methods = {
"frappe.www.contact.send_message": "erpnext.templates.utils.send_message"
}
welcome_email = "erpnext.setup.utils.welcome_email" welcome_email = "erpnext.setup.utils.welcome_email"
# setup wizard # setup wizard

View File

@ -9,15 +9,14 @@
"production_item_tab", "production_item_tab",
"item", "item",
"company", "company",
"item_name",
"uom", "uom",
"quantity",
"cb0", "cb0",
"is_active", "is_active",
"is_default", "is_default",
"allow_alternative_item", "allow_alternative_item",
"set_rate_of_sub_assembly_item_based_on_bom", "set_rate_of_sub_assembly_item_based_on_bom",
"project", "project",
"quantity",
"image", "image",
"currency_detail", "currency_detail",
"rm_cost_as_per", "rm_cost_as_per",
@ -27,6 +26,8 @@
"column_break_ivyw", "column_break_ivyw",
"currency", "currency",
"conversion_rate", "conversion_rate",
"materials_section",
"items",
"section_break_21", "section_break_21",
"operations_section_section", "operations_section_section",
"with_operations", "with_operations",
@ -38,8 +39,6 @@
"operating_cost_per_bom_quantity", "operating_cost_per_bom_quantity",
"operations_section", "operations_section",
"operations", "operations",
"materials_section",
"items",
"scrap_section", "scrap_section",
"scrap_items_section", "scrap_items_section",
"scrap_items", "scrap_items",
@ -59,6 +58,7 @@
"total_cost", "total_cost",
"base_total_cost", "base_total_cost",
"more_info_tab", "more_info_tab",
"item_name",
"description", "description",
"column_break_27", "column_break_27",
"has_variants", "has_variants",
@ -192,6 +192,7 @@
"options": "Quality Inspection Template" "options": "Quality Inspection Template"
}, },
{ {
"collapsible": 1,
"fieldname": "currency_detail", "fieldname": "currency_detail",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Cost Configuration" "label": "Cost Configuration"
@ -417,7 +418,7 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "website_section", "fieldname": "website_section",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Website" "label": "Website"
}, },
{ {
@ -482,7 +483,7 @@
{ {
"fieldname": "section_break_21", "fieldname": "section_break_21",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Operations & Materials" "label": "Operations"
}, },
{ {
"fieldname": "column_break_23", "fieldname": "column_break_23",
@ -605,7 +606,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-02-13 17:31:37.504565", "modified": "2023-04-06 12:47:58.514795",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

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

View File

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

View File

@ -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",

View File

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

View File

@ -22,17 +22,13 @@
"produced_qty", "produced_qty",
"process_loss_qty", "process_loss_qty",
"project", "project",
"serial_no_and_batch_for_finished_good_section", "section_break_ndpq",
"has_serial_no", "required_items",
"has_batch_no",
"column_break_17",
"serial_no",
"batch_size",
"work_order_configuration", "work_order_configuration",
"settings_section", "settings_section",
"allow_alternative_item", "allow_alternative_item",
"use_multi_level_bom", "use_multi_level_bom",
"column_break_18", "column_break_17",
"skip_transfer", "skip_transfer",
"from_wip_warehouse", "from_wip_warehouse",
"update_consumed_material_cost_in_project", "update_consumed_material_cost_in_project",
@ -42,9 +38,14 @@
"column_break_12", "column_break_12",
"fg_warehouse", "fg_warehouse",
"scrap_warehouse", "scrap_warehouse",
"serial_no_and_batch_for_finished_good_section",
"has_serial_no",
"has_batch_no",
"column_break_18",
"serial_no",
"batch_size",
"required_items_section", "required_items_section",
"materials_and_operations_tab", "materials_and_operations_tab",
"required_items",
"operations_section", "operations_section",
"operations", "operations",
"transfer_material_against", "transfer_material_against",
@ -586,7 +587,11 @@
{ {
"fieldname": "materials_and_operations_tab", "fieldname": "materials_and_operations_tab",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Materials & Operations" "label": "Operations"
},
{
"fieldname": "section_break_ndpq",
"fieldtype": "Section Break"
} }
], ],
"icon": "fa fa-cogs", "icon": "fa fa-cogs",
@ -594,7 +599,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-03 14:16:35.427731", "modified": "2023-04-06 12:35:12.149827",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",

View File

@ -330,3 +330,4 @@ erpnext.patches.v14_0.update_closing_balances
# below migration patches should always run last # below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger
execute:frappe.delete_doc_if_exists("Report", "Tax Detail") execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
erpnext.patches.v15_0.enable_all_leads

View File

@ -0,0 +1,8 @@
import frappe
def execute():
lead = frappe.qb.DocType("Lead")
frappe.qb.update(lead).set(lead.disabled, 0).set(lead.docstatus, 0).where(
lead.disabled == 1 and lead.docstatus == 1
).run()

View File

@ -55,6 +55,14 @@ frappe.ui.form.on(cur_frm.doctype, {
}, },
allocate_advances_automatically: function(frm) { allocate_advances_automatically: function(frm) {
frm.trigger('fetch_advances');
},
only_include_allocated_payments: function(frm) {
frm.trigger('fetch_advances');
},
fetch_advances: function(frm) {
if(frm.doc.allocate_advances_automatically) { if(frm.doc.allocate_advances_automatically) {
frappe.call({ frappe.call({
doc: frm.doc, doc: frm.doc,

View File

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

View File

@ -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 = [

View File

@ -3,18 +3,6 @@
if(!window.erpnext) window.erpnext = {}; if(!window.erpnext) window.erpnext = {};
// Add / update a new Lead / Communication
// subject, sender, description
frappe.send_message = function(opts, btn) {
return frappe.call({
type: "POST",
method: "erpnext.templates.utils.send_message",
btn: btn,
args: opts,
callback: opts.callback
});
};
erpnext.subscribe_to_newsletter = function(opts, btn) { erpnext.subscribe_to_newsletter = function(opts, btn) {
return frappe.call({ return frappe.call({
type: "POST", type: "POST",
@ -24,6 +12,3 @@ erpnext.subscribe_to_newsletter = function(opts, btn) {
callback: opts.callback callback: opts.callback
}); });
} }
// for backward compatibility
erpnext.send_message = frappe.send_message;

View File

@ -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 () {

View File

@ -11,10 +11,9 @@ from frappe.contacts.address_and_contact import (
delete_contact_and_address, delete_contact_and_address,
load_address_and_contact, load_address_and_contact,
) )
from frappe.desk.reportview import build_match_conditions, get_filters_cond
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from frappe.model.rename_doc import update_linked_doctypes from frappe.model.utils.rename_doc import update_linked_doctypes
from frappe.utils import cint, cstr, flt, get_formatted_email, today from frappe.utils import cint, cstr, flt, get_formatted_email, today
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
@ -445,50 +444,6 @@ def get_nested_links(link_doctype, link_name, ignore_permissions=False):
return links return links
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None):
from frappe.utils.deprecations import deprecation_warning
from erpnext.controllers.queries import get_fields
deprecation_warning(
"`get_customer_list` is deprecated and will be removed in version 15. Use `erpnext.controllers.queries.customer_query` instead."
)
fields = ["name", "customer_name", "customer_group", "territory"]
if frappe.db.get_default("cust_master_name") == "Customer Name":
fields = ["name", "customer_group", "territory"]
fields = get_fields("Customer", fields)
match_conditions = build_match_conditions("Customer")
match_conditions = "and {}".format(match_conditions) if match_conditions else ""
if filters:
filter_conditions = get_filters_cond(doctype, filters, [])
match_conditions += "{}".format(filter_conditions)
return frappe.db.sql(
"""
select %s
from `tabCustomer`
where docstatus < 2
and (%s like %s or customer_name like %s)
{match_conditions}
order by
case when name like %s then 0 else 1 end,
case when customer_name like %s then 0 else 1 end,
name, customer_name limit %s, %s
""".format(
match_conditions=match_conditions
)
% (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"),
("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len),
)
def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0): def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0):
credit_limit = get_credit_limit(customer, company) credit_limit = get_credit_limit(customer, company)
if not credit_limit: if not credit_limit:

View File

@ -29,6 +29,7 @@
"allow_multiple_items", "allow_multiple_items",
"allow_against_multiple_purchase_orders", "allow_against_multiple_purchase_orders",
"allow_sales_order_creation_for_expired_quotation", "allow_sales_order_creation_for_expired_quotation",
"dont_reserve_sales_order_qty_on_sales_return",
"hide_tax_id", "hide_tax_id",
"enable_discount_accounting" "enable_discount_accounting"
], ],
@ -186,6 +187,12 @@
"fieldname": "over_order_allowance", "fieldname": "over_order_allowance",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Over Order Allowance (%)" "label": "Over Order Allowance (%)"
},
{
"default": "0",
"fieldname": "dont_reserve_sales_order_qty_on_sales_return",
"fieldtype": "Check",
"label": "Don't Reserve Sales Order Qty on Sales Return"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@ -193,7 +200,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-03-03 11:16:54.333615", "modified": "2023-02-04 12:37:53.380857",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",
@ -222,4 +229,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -36,8 +36,24 @@ class ItemGroup(NestedSet, WebsiteGenerator):
self.make_route() self.make_route()
self.validate_item_group_defaults() self.validate_item_group_defaults()
self.check_item_tax()
ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True) ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True)
def check_item_tax(self):
"""Check whether Tax Rate is not entered twice for same Tax Type"""
check_list = []
for d in self.get("taxes"):
if d.item_tax_template:
if (d.item_tax_template, d.tax_category) in check_list:
frappe.throw(
_("{0} entered twice {1} in Item Taxes").format(
frappe.bold(d.item_tax_template),
"for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "",
)
)
else:
check_list.append((d.item_tax_template, d.tax_category))
def on_update(self): def on_update(self):
NestedSet.on_update(self) NestedSet.on_update(self)
invalidate_cache_for(self) invalidate_cache_for(self)
@ -148,12 +164,17 @@ def get_item_for_list_in_html(context):
def get_parent_item_groups(item_group_name, from_item=False): def get_parent_item_groups(item_group_name, from_item=False):
base_nav_page = {"name": _("All Products"), "route": "/all-products"} settings = frappe.get_cached_doc("E Commerce Settings")
if settings.enable_field_filters:
base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"}
else:
base_nav_page = {"name": _("All Products"), "route": "/all-products"}
if from_item and frappe.request.environ.get("HTTP_REFERER"): if from_item and frappe.request.environ.get("HTTP_REFERER"):
# base page after 'Home' will vary on Item page # base page after 'Home' will vary on Item page
last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0] last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
if last_page and last_page == "shop-by-category": if last_page and last_page in ("shop-by-category", "all-products"):
base_nav_page_title = " ".join(last_page.split("-")).title() base_nav_page_title = " ".join(last_page.split("-")).title()
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}

View File

@ -6,7 +6,8 @@ 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.query_builder.functions import CurDate, Sum, Timestamp
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
@ -176,45 +177,41 @@ def get_batch_qty(
:param warehouse: Optional - give qty for this warehouse :param warehouse: Optional - give qty for this warehouse
:param item_code: Optional - give qty for this item""" :param item_code: Optional - give qty for this item"""
sle = frappe.qb.DocType("Stock Ledger Entry")
out = 0 out = 0
if batch_no and warehouse: if batch_no and warehouse:
cond = "" query = (
if posting_date and posting_time: frappe.qb.from_(sle)
cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format( .select(Sum(sle.actual_qty))
posting_date, posting_time .where((sle.is_cancelled == 0) & (sle.warehouse == warehouse) & (sle.batch_no == batch_no))
)
if posting_date:
if posting_time is None:
posting_time = nowtime()
query = query.where(
Timestamp(sle.posting_date, sle.posting_time) <= Timestamp(posting_date, posting_time)
) )
out = float( out = query.run(as_list=True)[0][0] or 0
frappe.db.sql(
"""select sum(actual_qty)
from `tabStock Ledger Entry`
where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format(
cond
),
(warehouse, batch_no),
)[0][0]
or 0
)
if batch_no and not warehouse: if batch_no and not warehouse:
out = frappe.db.sql( out = (
"""select warehouse, sum(actual_qty) as qty frappe.qb.from_(sle)
from `tabStock Ledger Entry` .select(sle.warehouse, Sum(sle.actual_qty).as_("qty"))
where is_cancelled = 0 and batch_no=%s .where((sle.is_cancelled == 0) & (sle.batch_no == batch_no))
group by warehouse""", .groupby(sle.warehouse)
batch_no, ).run(as_dict=True)
as_dict=1,
)
if not batch_no and item_code and warehouse: if not batch_no and item_code and warehouse:
out = frappe.db.sql( out = (
"""select batch_no, sum(actual_qty) as qty frappe.qb.from_(sle)
from `tabStock Ledger Entry` .select(sle.batch_no, Sum(sle.actual_qty).as_("qty"))
where is_cancelled = 0 and item_code = %s and warehouse=%s .where((sle.is_cancelled == 0) & (sle.item_code == item_code) & (sle.warehouse == warehouse))
group by batch_no""", .groupby(sle.batch_no)
(item_code, warehouse), ).run(as_dict=True)
as_dict=1,
)
return out return out
@ -310,40 +307,44 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
cond = "" batch = frappe.qb.DocType("Batch")
sle = frappe.qb.DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(batch)
.join(sle)
.on(batch.batch_id == sle.batch_no)
.select(
batch.batch_id,
Sum(sle.actual_qty).as_("qty"),
)
.where(
(sle.item_code == item_code)
& (sle.warehouse == warehouse)
& (sle.is_cancelled == 0)
& ((batch.expiry_date >= CurDate()) | (batch.expiry_date.isnull()))
)
.groupby(batch.batch_id)
.orderby(batch.expiry_date, batch.creation)
)
if serial_no and frappe.get_cached_value("Item", item_code, "has_batch_no"): if serial_no and frappe.get_cached_value("Item", item_code, "has_batch_no"):
serial_nos = get_serial_nos(serial_no) serial_nos = get_serial_nos(serial_no)
batch = frappe.get_all( batches = frappe.get_all(
"Serial No", "Serial No",
fields=["distinct batch_no"], fields=["distinct batch_no"],
filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)}, filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)},
) )
if not batch: if not batches:
validate_serial_no_with_batch(serial_nos, item_code) validate_serial_no_with_batch(serial_nos, item_code)
if batch and len(batch) > 1: if batches and len(batches) > 1:
return [] return []
cond = " and `tabBatch`.name = %s" % (frappe.db.escape(batch[0].batch_no)) query = query.where(batch.name == batches[0].batch_no)
return frappe.db.sql( return query.run(as_dict=True)
"""
select batch_id, sum(`tabStock Ledger Entry`.actual_qty) as qty
from `tabBatch`
join `tabStock Ledger Entry` ignore index (item_code, warehouse)
on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s
and `tabStock Ledger Entry`.is_cancelled = 0
and (`tabBatch`.expiry_date >= CURRENT_DATE or `tabBatch`.expiry_date IS NULL) {0}
group by batch_id
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC
""".format(
cond
),
(item_code, warehouse),
as_dict=True,
)
def validate_serial_no_with_batch(serial_nos, item_code): def validate_serial_no_with_batch(serial_nos, item_code):

View File

@ -1180,6 +1180,53 @@ class TestDeliveryNote(FrappeTestCase):
self.assertTrue(return_dn.docstatus == 1) self.assertTrue(return_dn.docstatus == 1)
def test_reserve_qty_on_sales_return(self):
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0)
self.reserved_qty_check()
def test_dont_reserve_qty_on_sales_return(self):
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 1)
self.reserved_qty_check()
def reserved_qty_check(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from erpnext.stock.stock_balance import get_reserved_qty
dont_reserve_qty = frappe.db.get_single_value(
"Selling Settings", "dont_reserve_sales_order_qty_on_sales_return"
)
item = make_item().name
warehouse = "_Test Warehouse - _TC"
qty_to_reserve = 5
so = make_sales_order(item_code=item, qty=qty_to_reserve)
# Make qty avl for test.
make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, basic_rate=100)
# Test that item qty has been reserved on submit of sales order.
self.assertEqual(get_reserved_qty(item, warehouse), qty_to_reserve)
dn = make_delivery_note(so.name)
dn.save().submit()
# Test that item qty is no longer reserved since qty has been delivered.
self.assertEqual(get_reserved_qty(item, warehouse), 0)
dn_return = make_return_doc("Delivery Note", dn.name)
dn_return.save().submit()
returned = frappe.get_doc("Delivery Note", dn_return.name)
returned.update_prevdoc_status()
# Test that item qty is not reserved on sales return, if selling setting don't reserve qty is checked.
self.assertEqual(get_reserved_qty(item, warehouse), 0 if dont_reserve_qty else qty_to_reserve)
def tearDown(self):
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0)
def create_delivery_note(**args): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")

View File

@ -83,6 +83,8 @@
"actual_qty", "actual_qty",
"installed_qty", "installed_qty",
"item_tax_rate", "item_tax_rate",
"column_break_atna",
"received_qty",
"accounting_details_section", "accounting_details_section",
"expense_account", "expense_account",
"allow_zero_valuation_rate", "allow_zero_valuation_rate",
@ -832,13 +834,27 @@
"fieldname": "material_request_item", "fieldname": "material_request_item",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Material Request Item" "label": "Material Request Item"
},
{
"fieldname": "column_break_atna",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: parent.is_internal_customer",
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"report_hide": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-03-30 23:27:30.943175", "modified": "2023-04-06 09:28:29.182053",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",

View File

@ -117,7 +117,6 @@ class Item(Document):
self.validate_auto_reorder_enabled_in_stock_settings() self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change() self.cant_change()
self.validate_item_tax_net_rate_range() self.validate_item_tax_net_rate_range()
set_item_tax_from_hsn_code(self)
if not self.is_new(): if not self.is_new():
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
@ -352,10 +351,15 @@ class Item(Document):
check_list = [] check_list = []
for d in self.get("taxes"): for d in self.get("taxes"):
if d.item_tax_template: if d.item_tax_template:
if d.item_tax_template in check_list: if (d.item_tax_template, d.tax_category) in check_list:
frappe.throw(_("{0} entered twice in Item Tax").format(d.item_tax_template)) frappe.throw(
_("{0} entered twice {1} in Item Taxes").format(
frappe.bold(d.item_tax_template),
"for tax category {0}".format(frappe.bold(d.tax_category)) if d.tax_category else "",
)
)
else: else:
check_list.append(d.item_tax_template) check_list.append((d.item_tax_template, d.tax_category))
def validate_barcode(self): def validate_barcode(self):
import barcodenumber import barcodenumber
@ -1316,11 +1320,6 @@ def update_variants(variants, template, publish_progress=True):
frappe.publish_progress(count / total * 100, title=_("Updating Variants...")) frappe.publish_progress(count / total * 100, title=_("Updating Variants..."))
@erpnext.allow_regional
def set_item_tax_from_hsn_code(item):
pass
def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None: def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None:
for item_default in item_defaults: for item_default in item_defaults:
for doctype, field in [ for doctype, field in [

View File

@ -65,6 +65,16 @@ class PurchaseReceipt(BuyingController):
"percent_join_field": "purchase_invoice", "percent_join_field": "purchase_invoice",
"overflow_type": "receipt", "overflow_type": "receipt",
}, },
{
"source_dt": "Purchase Receipt Item",
"target_dt": "Delivery Note Item",
"join_field": "delivery_note_item",
"source_field": "received_qty",
"target_field": "received_qty",
"target_parent_dt": "Delivery Note",
"target_ref_field": "qty",
"overflow_type": "receipt",
},
] ]
if cint(self.is_return): if cint(self.is_return):

View File

@ -1544,6 +1544,72 @@ class TestPurchaseReceipt(FrappeTestCase):
res = get_item_details(args) res = get_item_details(args)
self.assertEqual(res.get("last_purchase_rate"), 100) self.assertEqual(res.get("last_purchase_rate"), 100)
def test_validate_received_qty_for_internal_pr(self):
prepare_data_for_internal_transfer()
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
# Step 1: Create Item
item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100})
# Step 2: Create Stock Entry (Material Receipt)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
make_stock_entry(
purpose="Material Receipt",
item_code=item.name,
qty=15,
company=company,
to_warehouse=from_warehouse,
)
# Step 3: Create Delivery Note with Internal Customer
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
dn = create_delivery_note(
item_code=item.name,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=10,
rate=100,
warehouse=from_warehouse,
target_warehouse=target_warehouse,
)
# Step 4: Create Internal Purchase Receipt
from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
pr = make_inter_company_purchase_receipt(dn.name)
pr.items[0].qty = 15
pr.items[0].from_warehouse = target_warehouse
pr.items[0].warehouse = to_warehouse
pr.items[0].rejected_warehouse = from_warehouse
pr.save()
self.assertRaises(OverAllowanceError, pr.submit)
# Step 5: Test Over Receipt Allowance
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
make_stock_entry(
purpose="Material Transfer",
item_code=item.name,
qty=5,
company=company,
from_warehouse=from_warehouse,
to_warehouse=target_warehouse,
)
pr.submit()
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@ -27,7 +27,6 @@
"set_posting_time", "set_posting_time",
"inspection_required", "inspection_required",
"apply_putaway_rule", "apply_putaway_rule",
"items_tab",
"bom_info_section", "bom_info_section",
"from_bom", "from_bom",
"use_multi_level_bom", "use_multi_level_bom",
@ -256,7 +255,7 @@
"description": "As per Stock UOM", "description": "As per Stock UOM",
"fieldname": "fg_completed_qty", "fieldname": "fg_completed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "For Quantity", "label": "Finished Good Quantity ",
"oldfieldname": "fg_completed_qty", "oldfieldname": "fg_completed_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"print_hide": 1 "print_hide": 1
@ -612,11 +611,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "items_tab", "collapsible": 1,
"fieldtype": "Tab Break",
"label": "Items"
},
{
"fieldname": "bom_info_section", "fieldname": "bom_info_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "BOM Info" "label": "BOM Info"
@ -644,8 +639,10 @@
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
{ {
"collapsible": 1,
"fieldname": "section_break_7qsm", "fieldname": "section_break_7qsm",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Process Loss"
}, },
{ {
"depends_on": "process_loss_percentage", "depends_on": "process_loss_percentage",
@ -677,7 +674,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-03 16:02:50.741816", "modified": "2023-04-06 12:42:56.673180",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry", "name": "Stock Entry",

View File

@ -4,7 +4,8 @@
from typing import Optional from typing import Optional
import frappe import frappe
from frappe import _, msgprint from frappe import _, bold, msgprint
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt from frappe.utils import cint, cstr, flt
import erpnext import erpnext
@ -89,7 +90,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 +141,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")))
@ -561,6 +570,54 @@ class StockReconciliation(StockController):
else: else:
self._cancel() self._cancel()
def recalculate_current_qty(self, item_code, batch_no):
for row in self.items:
if not (row.item_code == item_code and row.batch_no == batch_no):
continue
row.current_qty = get_batch_qty_for_stock_reco(item_code, row.warehouse, batch_no)
qty, val_rate = get_stock_balance(
item_code,
row.warehouse,
self.posting_date,
self.posting_time,
with_valuation_rate=True,
)
row.current_valuation_rate = val_rate
row.db_set(
{
"current_qty": row.current_qty,
"current_valuation_rate": row.current_valuation_rate,
"current_amount": flt(row.current_qty * row.current_valuation_rate),
}
)
def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no):
ledger = frappe.qb.DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(ledger)
.select(
Sum(ledger.actual_qty).as_("batch_qty"),
)
.where(
(ledger.item_code == item_code)
& (ledger.warehouse == warehouse)
& (ledger.docstatus == 1)
& (ledger.is_cancelled == 0)
& (ledger.batch_no == batch_no)
)
.groupby(ledger.batch_no)
)
sle = query.run(as_dict=True)
return flt(sle[0].batch_qty) if sle else 0
@frappe.whitelist() @frappe.whitelist()
def get_items( def get_items(

View File

@ -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):
@ -620,7 +637,9 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
taxes_with_no_validity.append(tax) taxes_with_no_validity.append(tax)
if taxes_with_validity: if taxes_with_validity:
taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from, reverse=True) taxes = sorted(
taxes_with_validity, key=lambda i: i.valid_from or tax.maximum_net_rate, reverse=True
)
else: else:
taxes = taxes_with_no_validity taxes = taxes_with_no_validity

View File

@ -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
@ -331,6 +331,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()]
@ -339,9 +367,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:
@ -372,11 +399,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

View File

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

View File

@ -94,10 +94,13 @@ def get_balance_qty_from_sle(item_code, warehouse):
def get_reserved_qty(item_code, warehouse): def get_reserved_qty(item_code, warehouse):
dont_reserve_on_return = frappe.get_cached_value(
"Selling Settings", "Selling Settings", "dont_reserve_sales_order_qty_on_sales_return"
)
reserved_qty = frappe.db.sql( reserved_qty = frappe.db.sql(
""" f"""
select select
sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty) / so_item_qty)) sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty - if(dont_reserve_qty_on_return, so_item_returned_qty, 0)) / so_item_qty))
from from
( (
(select (select
@ -112,6 +115,12 @@ def get_reserved_qty(item_code, warehouse):
where name = dnpi.parent_detail_docname where name = dnpi.parent_detail_docname
and delivered_by_supplier = 0 and delivered_by_supplier = 0
) as so_item_delivered_qty, ) as so_item_delivered_qty,
(
select returned_qty from `tabSales Order Item`
where name = dnpi.parent_detail_docname
and delivered_by_supplier = 0
) as so_item_returned_qty,
{dont_reserve_on_return} as dont_reserve_qty_on_return,
parent, name parent, name
from from
( (
@ -125,7 +134,9 @@ def get_reserved_qty(item_code, warehouse):
) dnpi) ) dnpi)
union union
(select stock_qty as dnpi_qty, qty as so_item_qty, (select stock_qty as dnpi_qty, qty as so_item_qty,
delivered_qty as so_item_delivered_qty, parent, name delivered_qty as so_item_delivered_qty,
returned_qty as so_item_returned_qty,
{dont_reserve_on_return}, parent, name
from `tabSales Order Item` so_item from `tabSales Order Item` so_item
where item_code = %s and warehouse = %s where item_code = %s and warehouse = %s
and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0) and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0)

View File

@ -1337,6 +1337,9 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
next_stock_reco_detail = get_next_stock_reco(args) next_stock_reco_detail = get_next_stock_reco(args)
if next_stock_reco_detail: if next_stock_reco_detail:
detail = next_stock_reco_detail[0] detail = next_stock_reco_detail[0]
if detail.batch_no:
regenerate_sle_for_batch_stock_reco(detail)
# add condition to update SLEs before this date & time # add condition to update SLEs before this date & time
datetime_limit_condition = get_datetime_limit_condition(detail) datetime_limit_condition = get_datetime_limit_condition(detail)
@ -1364,6 +1367,16 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
validate_negative_qty_in_future_sle(args, allow_negative_stock) validate_negative_qty_in_future_sle(args, allow_negative_stock)
def regenerate_sle_for_batch_stock_reco(detail):
doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
doc.docstatus = 2
doc.update_stock_ledger()
doc.recalculate_current_qty(detail.item_code, detail.batch_no)
doc.docstatus = 1
doc.update_stock_ledger()
def get_stock_reco_qty_shift(args): def get_stock_reco_qty_shift(args):
stock_reco_qty_shift = 0 stock_reco_qty_shift = 0
if args.get("is_cancelled"): if args.get("is_cancelled"):
@ -1393,7 +1406,7 @@ def get_next_stock_reco(args):
return frappe.db.sql( return frappe.db.sql(
""" """
select select
name, posting_date, posting_time, creation, voucher_no name, posting_date, posting_time, creation, voucher_no, item_code, batch_no, actual_qty
from from
`tabStock Ledger Entry` `tabStock Ledger Entry`
where where

View File

@ -245,17 +245,17 @@ class SubcontractingReceipt(SubcontractingController):
item.expense_account = expense_account item.expense_account = expense_account
def update_status(self, status=None, update_modified=False): def update_status(self, status=None, update_modified=False):
if self.docstatus >= 1 and not status: if not status:
if self.docstatus == 1: if self.docstatus == 0:
status = "Draft"
elif self.docstatus == 1:
status = "Completed"
if self.is_return: if self.is_return:
status = "Return" status = "Return"
return_against = frappe.get_doc("Subcontracting Receipt", self.return_against) return_against = frappe.get_doc("Subcontracting Receipt", self.return_against)
return_against.run_method("update_status") return_against.run_method("update_status")
else: elif self.per_returned == 100:
if self.per_returned == 100: status = "Return Issued"
status = "Return Issued"
elif self.status == "Draft":
status = "Completed"
elif self.docstatus == 2: elif self.docstatus == 2:
status = "Cancelled" status = "Cancelled"

View File

@ -6,13 +6,12 @@ import frappe
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def send_message(subject="Website Query", message="", sender="", status="Open"): def send_message(sender, message, subject="Website Query"):
from frappe.www.contact import send_message as website_send_message from frappe.www.contact import send_message as website_send_message
website_send_message(sender, message, subject)
lead = customer = None lead = customer = None
website_send_message(subject, message, sender)
customer = frappe.db.sql( customer = frappe.db.sql(
"""select distinct dl.link_name from `tabDynamic Link` dl """select distinct dl.link_name from `tabDynamic Link` dl
left join `tabContact` c on dl.parent=c.name where dl.link_doctype='Customer' left join `tabContact` c on dl.parent=c.name where dl.link_doctype='Customer'
@ -58,5 +57,3 @@ def send_message(subject="Website Query", message="", sender="", status="Open"):
} }
) )
comm.insert(ignore_permissions=True) comm.insert(ignore_permissions=True)
return "okay"

View File

@ -1875,6 +1875,7 @@ Parents Teacher Meeting Attendance,Eltern Lehrer Treffen Teilnahme,
Part-time,Teilzeit, Part-time,Teilzeit,
Partially Depreciated,Teilweise abgeschrieben, Partially Depreciated,Teilweise abgeschrieben,
Partially Received,Teilweise erhalten, Partially Received,Teilweise erhalten,
Partly Paid,Teilweise bezahlt,
Party,Partei, Party,Partei,
Party Name,Name der Partei, Party Name,Name der Partei,
Party Type,Partei-Typ, Party Type,Partei-Typ,

Can't render this file because it is too large.

View File

@ -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,
) )

View File

@ -53,6 +53,7 @@ def get_tabs(categories):
def get_category_records(categories: list): def get_category_records(categories: list):
categorical_data = {} categorical_data = {}
website_item_meta = frappe.get_meta("Website Item", cached=True)
for c in categories: for c in categories:
if c == "item_group": if c == "item_group":
@ -64,7 +65,16 @@ def get_category_records(categories: list):
continue continue
doctype = frappe.unscrub(c) field_type = website_item_meta.get_field(c).fieldtype
if field_type == "Table MultiSelect":
child_doc = website_item_meta.get_field(c).options
for field in frappe.get_meta(child_doc, cached=True).fields:
if field.fieldtype == "Link" and field.reqd:
doctype = field.options
else:
doctype = website_item_meta.get_field(c).options
fields = ["name"] fields = ["name"]
try: try: