mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-26 18:18:30 +00:00
Merge pull request #45860 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -49,6 +49,7 @@ class AccountingDimension(Document):
|
||||
"Accounting Dimension Detail",
|
||||
"Company",
|
||||
"Account",
|
||||
"Finance Book",
|
||||
):
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
|
||||
@@ -129,7 +129,7 @@ class GLEntry(Document):
|
||||
if not self.get(k):
|
||||
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
|
||||
|
||||
if not (self.party_type and self.party):
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
|
||||
@@ -812,27 +812,41 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
paid_amount: function (frm) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (!frm.doc.received_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
}
|
||||
}
|
||||
frm.trigger("reset_received_amount");
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
},
|
||||
|
||||
received_amount: function (frm) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
frm.set_paid_amount_based_on_received_amount = true;
|
||||
|
||||
if (!frm.doc.paid_amount && frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
}
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
}
|
||||
|
||||
frm.set_value(
|
||||
"base_received_amount",
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
|
||||
if (!frm.doc.paid_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
}
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.payment_type == "Pay")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
|
||||
else frm.events.set_unallocated_amount(frm);
|
||||
|
||||
@@ -224,6 +224,7 @@
|
||||
"label": "Accounts"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "party",
|
||||
"fieldname": "party_balance",
|
||||
"fieldtype": "Currency",
|
||||
@@ -253,6 +254,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "paid_from",
|
||||
"fieldname": "paid_from_account_balance",
|
||||
"fieldtype": "Currency",
|
||||
@@ -286,6 +288,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "paid_to",
|
||||
"fieldname": "paid_to_account_balance",
|
||||
"fieldtype": "Currency",
|
||||
@@ -806,7 +809,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-01-13 16:03:47.169699",
|
||||
"modified": "2025-01-31 17:27:28.555246",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -25,6 +25,10 @@ from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
|
||||
get_party_account_based_on_invoice_discounting,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
@@ -114,6 +118,23 @@ class PaymentEntry(AccountsController):
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def validate_for_repost(self):
|
||||
validate_docs_for_voucher_types(["Payment Entry"])
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def on_update_after_submit(self):
|
||||
# Flag will be set on Reconciliation
|
||||
# Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost.
|
||||
if self.flags.get("ignore_reposting_on_reconciliation"):
|
||||
return
|
||||
|
||||
self.needs_repost = self.check_if_fields_updated(
|
||||
fields_to_check=[], child_tables={"references": [], "taxes": [], "deductions": []}
|
||||
)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def set_liability_account(self):
|
||||
# Auto setting liability account should only be done during 'draft' status
|
||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||
|
||||
@@ -299,7 +299,8 @@
|
||||
"oldfieldname": "project_name",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Project",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -2186,7 +2187,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-01-14 11:38:30.446370",
|
||||
"modified": "2025-02-06 15:59:54.636202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -270,7 +270,10 @@ def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
|
||||
|
||||
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
parties,
|
||||
tax_details,
|
||||
inv.company,
|
||||
party_type=party_type,
|
||||
)
|
||||
|
||||
payment_entry_vouchers = get_payment_entry_vouchers(
|
||||
@@ -360,11 +363,23 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
voucher_wise_amount = []
|
||||
vouchers = []
|
||||
|
||||
ldcs = frappe.db.get_all(
|
||||
"Lower Deduction Certificate",
|
||||
filters={
|
||||
"valid_from": [">=", tax_details.from_date],
|
||||
"valid_upto": ["<=", tax_details.to_date],
|
||||
"company": company,
|
||||
"supplier": ["in", parties],
|
||||
},
|
||||
fields=["supplier", "valid_from", "valid_upto", "rate"],
|
||||
)
|
||||
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
field = [
|
||||
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total",
|
||||
"name",
|
||||
"grand_total",
|
||||
"posting_date",
|
||||
]
|
||||
|
||||
filters = {
|
||||
@@ -383,18 +398,23 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
invoices_details = frappe.get_all(doctype, filters=filters, fields=field)
|
||||
|
||||
for d in invoices_details:
|
||||
vouchers.append(d.name)
|
||||
voucher_wise_amount.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_name": d.name,
|
||||
"voucher_type": doctype,
|
||||
"taxable_amount": d.base_net_total,
|
||||
"grand_total": d.grand_total,
|
||||
}
|
||||
)
|
||||
d = frappe._dict(
|
||||
{
|
||||
"voucher_name": d.name,
|
||||
"voucher_type": doctype,
|
||||
"taxable_amount": d.base_net_total,
|
||||
"grand_total": d.grand_total,
|
||||
"posting_date": d.posting_date,
|
||||
}
|
||||
)
|
||||
|
||||
if ldc := [x for x in ldcs if d.posting_date >= x.valid_from and d.posting_date <= x.valid_upto]:
|
||||
if ldc[0].supplier in parties and ldc[0].rate == 0:
|
||||
d.update({"taxable_amount": 0})
|
||||
|
||||
vouchers.append(d.voucher_name)
|
||||
voucher_wise_amount.append(d)
|
||||
|
||||
journal_entries_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT j.name, ja.credit - ja.debit AS amount, ja.reference_type
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
from frappe.utils import add_days, add_months, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -666,6 +666,49 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
pi2.cancel()
|
||||
pi3.cancel()
|
||||
|
||||
def test_ldc_at_0_rate(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier",
|
||||
"Test LDC Supplier",
|
||||
{
|
||||
"tax_withholding_category": "Test Service Category",
|
||||
"pan": "ABCTY1234D",
|
||||
},
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
valid_from = fiscal_year[1]
|
||||
valid_upto = add_months(valid_from, 1)
|
||||
create_lower_deduction_certificate(
|
||||
supplier="Test LDC Supplier",
|
||||
certificate_no="1AE0423AAJ",
|
||||
tax_withholding_category="Test Service Category",
|
||||
tax_rate=0,
|
||||
limit=50000,
|
||||
valid_from=valid_from,
|
||||
valid_upto=valid_upto,
|
||||
)
|
||||
|
||||
pi1 = create_purchase_invoice(
|
||||
supplier="Test LDC Supplier", rate=35000, posting_date=valid_from, set_posting_time=True
|
||||
)
|
||||
pi1.submit()
|
||||
self.assertEqual(pi1.taxes, [])
|
||||
|
||||
pi2 = create_purchase_invoice(
|
||||
supplier="Test LDC Supplier",
|
||||
rate=35000,
|
||||
posting_date=add_days(valid_upto, 1),
|
||||
set_posting_time=True,
|
||||
)
|
||||
pi2.submit()
|
||||
self.assertEqual(len(pi2.taxes), 1)
|
||||
# pi1 net total shouldn't be included as it lies within LDC at rate of '0'
|
||||
self.assertEqual(pi2.taxes[0].tax_amount, 3500)
|
||||
|
||||
pi1.cancel()
|
||||
pi2.cancel()
|
||||
|
||||
def set_previous_fy_and_tax_category(self):
|
||||
test_company = "_Test Company"
|
||||
category = "Cumulative Threshold TDS"
|
||||
@@ -823,7 +866,8 @@ def create_purchase_invoice(**args):
|
||||
pi = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Invoice",
|
||||
"posting_date": today(),
|
||||
"set_posting_time": args.set_posting_time or False,
|
||||
"posting_date": args.posting_date or today(),
|
||||
"apply_tds": 0 if args.do_not_apply_tds else 1,
|
||||
"supplier": args.supplier,
|
||||
"company": "_Test Company",
|
||||
@@ -1161,7 +1205,9 @@ def create_tax_withholding_category(
|
||||
).insert()
|
||||
|
||||
|
||||
def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_rate, certificate_no, limit):
|
||||
def create_lower_deduction_certificate(
|
||||
supplier, tax_withholding_category, tax_rate, certificate_no, limit, valid_from=None, valid_upto=None
|
||||
):
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
if not frappe.db.exists("Lower Deduction Certificate", certificate_no):
|
||||
frappe.get_doc(
|
||||
@@ -1172,8 +1218,8 @@ def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_r
|
||||
"certificate_no": certificate_no,
|
||||
"tax_withholding_category": tax_withholding_category,
|
||||
"fiscal_year": fiscal_year[0],
|
||||
"valid_from": fiscal_year[1],
|
||||
"valid_upto": fiscal_year[2],
|
||||
"valid_from": valid_from or fiscal_year[1],
|
||||
"valid_upto": valid_upto or fiscal_year[2],
|
||||
"rate": tax_rate,
|
||||
"certificate_limit": limit,
|
||||
}
|
||||
|
||||
@@ -680,11 +680,15 @@ def make_reverse_gl_entries(
|
||||
|
||||
debit_in_account_currency = new_gle.get("debit_in_account_currency", 0)
|
||||
credit_in_account_currency = new_gle.get("credit_in_account_currency", 0)
|
||||
debit_in_transaction_currency = new_gle.get("debit_in_transaction_currency", 0)
|
||||
credit_in_transaction_currency = new_gle.get("credit_in_transaction_currency", 0)
|
||||
|
||||
new_gle["debit"] = credit
|
||||
new_gle["credit"] = debit
|
||||
new_gle["debit_in_account_currency"] = credit_in_account_currency
|
||||
new_gle["credit_in_account_currency"] = debit_in_account_currency
|
||||
new_gle["debit_in_transaction_currency"] = credit_in_transaction_currency
|
||||
new_gle["credit_in_transaction_currency"] = debit_in_transaction_currency
|
||||
|
||||
new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
|
||||
new_gle["is_cancelled"] = 1
|
||||
|
||||
@@ -219,15 +219,34 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
|
||||
|
||||
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
|
||||
for src in gross_profit_data.grouped_data:
|
||||
row = []
|
||||
for col in group_wise_columns.get(scrub(filters.group_by)):
|
||||
row.append(src.get(col))
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
|
||||
row.append(filters.currency)
|
||||
group_columns = group_wise_columns.get(scrub(filters.group_by))
|
||||
|
||||
for src in gross_profit_data.grouped_data:
|
||||
total_base_amount += src.base_amount or 0.00
|
||||
total_buying_amount += src.buying_amount or 0.00
|
||||
|
||||
row = [src.get(col) for col in group_columns] + [filters.currency]
|
||||
|
||||
data.append(row)
|
||||
|
||||
total_gross_profit = total_base_amount - total_buying_amount
|
||||
currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
|
||||
gross_profit_percent = (total_gross_profit / total_base_amount * 100.0) if total_base_amount else 0
|
||||
|
||||
total_row = {
|
||||
group_columns[0]: "Total",
|
||||
"base_amount": total_base_amount,
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"gross_profit_percent": flt(gross_profit_percent, currency_precision),
|
||||
}
|
||||
|
||||
total_row = [total_row.get(col, None) for col in [*group_columns, "currency"]]
|
||||
data.append(total_row)
|
||||
|
||||
|
||||
def get_columns(group_wise_columns, filters):
|
||||
columns = []
|
||||
|
||||
@@ -773,6 +773,8 @@ def update_reference_in_payment_entry(
|
||||
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
|
||||
)
|
||||
|
||||
# Ledgers will be reposted by Reconciliation tool
|
||||
payment_entry.flags.ignore_reposting_on_reconciliation = True
|
||||
if not do_not_save:
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
return row, update_advance_paid
|
||||
|
||||
@@ -394,7 +394,11 @@ def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=
|
||||
},
|
||||
"Request for Quotation Item": {
|
||||
"doctype": "Supplier Quotation Item",
|
||||
"field_map": {"name": "request_for_quotation_item", "parent": "request_for_quotation"},
|
||||
"field_map": {
|
||||
"name": "request_for_quotation_item",
|
||||
"parent": "request_for_quotation",
|
||||
"project_name": "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -821,7 +821,7 @@ class AccountsController(TransactionBase):
|
||||
and item.get("use_serial_batch_fields")
|
||||
)
|
||||
):
|
||||
if fieldname == "batch_no" and not item.batch_no:
|
||||
if fieldname == "batch_no" and not item.batch_no and not item.is_free_item:
|
||||
item.set("rate", ret.get("rate"))
|
||||
item.set("price_list_rate", ret.get("price_list_rate"))
|
||||
item.set(fieldname, value)
|
||||
@@ -1903,22 +1903,22 @@ class AccountsController(TransactionBase):
|
||||
continue
|
||||
|
||||
ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
|
||||
based_on_amt = flt(item.get(based_on))
|
||||
|
||||
if not ref_amt:
|
||||
frappe.msgprint(
|
||||
_("System will not check over billing since amount for Item {0} in {1} is zero").format(
|
||||
item.item_code, ref_dt
|
||||
),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
if based_on_amt: # Skip warning for free items
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
||||
).format(item.item_code, ref_dt),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
continue
|
||||
|
||||
already_billed = self.get_billed_amount_for_item(item, item_ref_dn, based_on)
|
||||
|
||||
total_billed_amt = flt(
|
||||
flt(already_billed) + flt(item.get(based_on)), self.precision(based_on, item)
|
||||
)
|
||||
total_billed_amt = flt(flt(already_billed) + based_on_amt, self.precision(based_on, item))
|
||||
|
||||
allowance, item_allowance, global_qty_allowance, global_amount_allowance = get_allowance_for(
|
||||
item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount"
|
||||
|
||||
@@ -1620,7 +1620,9 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
|
||||
"project": work_order.project,
|
||||
"company": work_order.company,
|
||||
"sequence_id": row.get("sequence_id"),
|
||||
"wip_warehouse": work_order.wip_warehouse,
|
||||
"wip_warehouse": work_order.wip_warehouse or row.get("wip_warehouse")
|
||||
if not work_order.skip_transfer or work_order.from_wip_warehouse
|
||||
else work_order.source_warehouse or row.get("source_warehouse"),
|
||||
"hour_rate": row.get("hour_rate"),
|
||||
"serial_no": row.get("serial_no"),
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ def update_reference_reports(reference_report):
|
||||
|
||||
|
||||
def update_report_json(report):
|
||||
report_json = json.loads(report.json)
|
||||
report_json = json.loads(report.json) if report.get("json") else {}
|
||||
report_filter = report_json.get("filters")
|
||||
|
||||
if not report_filter:
|
||||
|
||||
@@ -11,16 +11,17 @@ def execute():
|
||||
frappe.db.set_single_value("Accounts Settings", "reconciliation_queue_size", 5)
|
||||
|
||||
# Create Scheduler Event record if it doesn't exist
|
||||
method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs"
|
||||
if not frappe.db.get_all(
|
||||
"Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method}
|
||||
):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Scheduler Event",
|
||||
"scheduled_against": "Process Payment Reconciliation",
|
||||
"method": method,
|
||||
}
|
||||
).save()
|
||||
if frappe.reload_doc("core", "doctype", "scheduler_event"):
|
||||
method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs"
|
||||
if not frappe.db.get_all(
|
||||
"Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method}
|
||||
):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Scheduler Event",
|
||||
"scheduled_against": "Process Payment Reconciliation",
|
||||
"method": method,
|
||||
}
|
||||
).save()
|
||||
|
||||
sync_auto_reconcile_config(15)
|
||||
sync_auto_reconcile_config(15)
|
||||
|
||||
@@ -86,8 +86,6 @@ class Project(Document):
|
||||
),
|
||||
)
|
||||
|
||||
self.update_costing()
|
||||
|
||||
def before_print(self, settings=None):
|
||||
self.onload()
|
||||
|
||||
|
||||
@@ -331,22 +331,19 @@ def sales_invoice_on_submit(doc, method):
|
||||
]:
|
||||
return
|
||||
|
||||
if not len(doc.payment_schedule):
|
||||
frappe.throw(_("Please set the Payment Schedule"), title=_("E-Invoicing Information Missing"))
|
||||
else:
|
||||
for schedule in doc.payment_schedule:
|
||||
if not schedule.mode_of_payment:
|
||||
frappe.throw(
|
||||
_("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx),
|
||||
title=_("E-Invoicing Information Missing"),
|
||||
)
|
||||
elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"):
|
||||
frappe.throw(
|
||||
_("Row {0}: Please set the correct code on Mode of Payment {1}").format(
|
||||
schedule.idx, schedule.mode_of_payment
|
||||
),
|
||||
title=_("E-Invoicing Information Missing"),
|
||||
)
|
||||
for schedule in doc.payment_schedule:
|
||||
if not schedule.mode_of_payment:
|
||||
frappe.throw(
|
||||
_("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx),
|
||||
title=_("E-Invoicing Information Missing"),
|
||||
)
|
||||
elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"):
|
||||
frappe.throw(
|
||||
_("Row {0}: Please set the correct code on Mode of Payment {1}").format(
|
||||
schedule.idx, schedule.mode_of_payment
|
||||
),
|
||||
title=_("E-Invoicing Information Missing"),
|
||||
)
|
||||
|
||||
prepare_and_attach_invoice(doc)
|
||||
|
||||
|
||||
@@ -1151,7 +1151,8 @@
|
||||
"label": "Project",
|
||||
"oldfieldname": "project",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Project"
|
||||
"options": "Project",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_account_currency",
|
||||
@@ -1654,7 +1655,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-26 12:42:06.872527",
|
||||
"modified": "2025-02-06 16:02:20.320877",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
@@ -1732,4 +1733,4 @@
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -748,6 +748,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
frappe.utils.play_sound("error");
|
||||
return;
|
||||
}
|
||||
this.highlight_numpad_btn($btn, current_action);
|
||||
|
||||
if (first_click_event || field_to_edit_changed) {
|
||||
this.prev_action = current_action;
|
||||
@@ -793,7 +794,6 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
this.numpad_value = current_action;
|
||||
}
|
||||
|
||||
this.highlight_numpad_btn($btn, current_action);
|
||||
this.events.numpad_event(this.numpad_value, this.prev_action);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
}
|
||||
|
||||
make_invoice_fields_control() {
|
||||
this.reqd_invoice_fields = [];
|
||||
frappe.db.get_doc("POS Settings", undefined).then((doc) => {
|
||||
const fields = doc.invoice_fields;
|
||||
if (!fields.length) return;
|
||||
@@ -67,6 +68,9 @@ erpnext.PointOfSale.Payment = class {
|
||||
},
|
||||
};
|
||||
}
|
||||
if (df.reqd && (df.fieldtype !== "Button" || !df.read_only)) {
|
||||
this.reqd_invoice_fields.push({ fieldname: df.fieldname, label: df.label });
|
||||
}
|
||||
|
||||
this[`${df.fieldname}_field`] = frappe.ui.form.make_control({
|
||||
df: {
|
||||
@@ -204,7 +208,11 @@ erpnext.PointOfSale.Payment = class {
|
||||
const paid_amount = doc.paid_amount;
|
||||
const items = doc.items;
|
||||
|
||||
if (paid_amount == 0 || !items.length) {
|
||||
if (!this.validate_reqd_invoice_fields()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!items.length || (paid_amount == 0 && doc.additional_discount_percentage != 100)) {
|
||||
const message = items.length
|
||||
? __("You cannot submit the order without payment.")
|
||||
: __("You cannot submit empty order.");
|
||||
@@ -620,4 +628,20 @@ erpnext.PointOfSale.Payment = class {
|
||||
.replace(/^[^_a-zA-Z\p{L}]+/u, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
validate_reqd_invoice_fields() {
|
||||
const doc = this.events.get_frm().doc;
|
||||
let validation_flag = true;
|
||||
for (let field of this.reqd_invoice_fields) {
|
||||
if (!doc[field.fieldname]) {
|
||||
validation_flag = false;
|
||||
frappe.show_alert({
|
||||
message: __("{0} is a mandatory field.", [field.label]),
|
||||
indicator: "orange",
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
}
|
||||
}
|
||||
return validation_flag;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -182,8 +182,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "user_id.user_image",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
@@ -824,7 +822,7 @@
|
||||
"image_field": "image",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-03 17:36:20.984421",
|
||||
"modified": "2025-02-07 13:54:40.122345",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Employee",
|
||||
@@ -873,4 +871,4 @@
|
||||
"states": [],
|
||||
"title_field": "employee_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -64,14 +64,12 @@ class Employee(NestedSet):
|
||||
|
||||
def validate_user_details(self):
|
||||
if self.user_id:
|
||||
data = frappe.db.get_value("User", self.user_id, ["enabled", "user_image"], as_dict=1)
|
||||
data = frappe.db.get_value("User", self.user_id, ["enabled"], as_dict=1)
|
||||
|
||||
if not data:
|
||||
self.user_id = None
|
||||
return
|
||||
|
||||
if data.get("user_image") and self.image == "":
|
||||
self.image = data.get("user_image")
|
||||
self.validate_for_enabled_user_id(data.get("enabled", 0))
|
||||
self.validate_duplicate_user_id()
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@ def make_request_for_quotation(source_name, target_doc=None):
|
||||
"field_map": [
|
||||
["name", "material_request_item"],
|
||||
["parent", "material_request"],
|
||||
["uom", "uom"],
|
||||
["project", "project_name"],
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1360,26 +1360,25 @@ def get_item_account_wise_additional_cost(purchase_document):
|
||||
for item in landed_cost_voucher_doc.items:
|
||||
if item.receipt_document == purchase_document:
|
||||
for account in landed_cost_voucher_doc.taxes:
|
||||
exchange_rate = account.exchange_rate or 1
|
||||
item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {})
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(
|
||||
account.expense_account, {"amount": 0.0, "base_amount": 0.0}
|
||||
)
|
||||
|
||||
if total_item_cost > 0:
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]["amount"] += account.amount * item.get(based_on_field) / total_item_cost
|
||||
item_row = item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]
|
||||
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]["base_amount"] += account.base_amount * item.get(based_on_field) / total_item_cost
|
||||
if total_item_cost > 0:
|
||||
item_row["amount"] += account.amount * item.get(based_on_field) / total_item_cost
|
||||
|
||||
item_row["base_amount"] += (
|
||||
account.base_amount * item.get(based_on_field) / total_item_cost
|
||||
)
|
||||
else:
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]["amount"] += item.applicable_charges
|
||||
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
|
||||
account.expense_account
|
||||
]["base_amount"] += item.applicable_charges
|
||||
item_row["amount"] += item.applicable_charges / exchange_rate
|
||||
item_row["base_amount"] += item.applicable_charges
|
||||
|
||||
return item_account_wise_cost
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"autoname": "hash",
|
||||
"creation": "2023-08-11 17:22:12.907518",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_details_tab",
|
||||
"naming_series",
|
||||
"company",
|
||||
"item_name",
|
||||
"has_serial_no",
|
||||
@@ -152,6 +151,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "avg_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Avg Rate",
|
||||
@@ -159,6 +159,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Amount",
|
||||
@@ -166,6 +167,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "total_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Qty",
|
||||
@@ -195,12 +197,6 @@
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Naming Series",
|
||||
"options": "SABB-.########"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.voucher_type == \"Purchase Receipt\"",
|
||||
@@ -251,11 +247,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-15 15:22:24.003486",
|
||||
"modified": "2025-02-12 10:53:32.090309",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial and Batch Bundle",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -389,4 +385,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "item_code"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,9 +55,7 @@ class SerialandBatchBundle(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.stock.doctype.serial_and_batch_entry.serial_and_batch_entry import (
|
||||
SerialandBatchEntry,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_and_batch_entry.serial_and_batch_entry import SerialandBatchEntry
|
||||
|
||||
amended_from: DF.Link | None
|
||||
avg_rate: DF.Float
|
||||
@@ -70,7 +68,6 @@ class SerialandBatchBundle(Document):
|
||||
item_code: DF.Link
|
||||
item_group: DF.Link | None
|
||||
item_name: DF.Data | None
|
||||
naming_series: DF.Literal["SABB-.########"]
|
||||
posting_date: DF.Date | None
|
||||
posting_time: DF.Time | None
|
||||
returned_against: DF.Data | None
|
||||
|
||||
@@ -295,6 +295,11 @@ frappe.ui.form.on("Stock Reconciliation Item", {
|
||||
|
||||
qty: function (frm, cdt, cdn) {
|
||||
frm.events.set_amount_quantity(frm, cdt, cdn);
|
||||
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.use_serial_batch_fields && !row.qty && row.serial_no) {
|
||||
frappe.model.set_value(cdt, cdn, "serial_no", "");
|
||||
}
|
||||
},
|
||||
|
||||
valuation_rate: function (frm, cdt, cdn) {
|
||||
|
||||
@@ -1372,13 +1372,13 @@ def get_stock_balance_for(
|
||||
or 0
|
||||
)
|
||||
|
||||
if row.use_serial_batch_fields and row.batch_no:
|
||||
if row.use_serial_batch_fields and row.batch_no and (qty or row.current_qty):
|
||||
rate = get_incoming_rate(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"qty": row.qty * -1,
|
||||
"qty": flt(qty or row.current_qty) * -1,
|
||||
"batch_no": row.batch_no,
|
||||
"company": company,
|
||||
"posting_date": posting_date,
|
||||
|
||||
@@ -1408,6 +1408,44 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
|
||||
self.assertFalse(sr.items[0].serial_and_batch_bundle)
|
||||
|
||||
def test_stock_reco_batch_item_current_valuation(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
# Add new serial nos
|
||||
item_code = "Stock-Reco-batch-Item-1234"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
self.make_item(
|
||||
item_code,
|
||||
frappe._dict(
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "JJ-SRI1234-.#####",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
target=warehouse,
|
||||
qty=1,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
sr = create_stock_reconciliation(
|
||||
item_code=item_code, warehouse=warehouse, qty=0, rate=100, do_not_save=1
|
||||
)
|
||||
|
||||
sr.items[0].batch_no = batch_no
|
||||
sr.items[0].use_serial_batch_fields = 1
|
||||
sr.save()
|
||||
self.assertEqual(sr.items[0].current_valuation_rate, 100)
|
||||
self.assertEqual(sr.difference_amount, 100 * -1)
|
||||
self.assertTrue(sr.items[0].qty == 0)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
||||
@@ -187,6 +187,9 @@ def update_stock(ctx, out, doc=None):
|
||||
and out.warehouse
|
||||
and out.stock_qty > 0
|
||||
):
|
||||
if doc and isinstance(doc, dict):
|
||||
doc = frappe._dict(doc)
|
||||
|
||||
kwargs = frappe._dict(
|
||||
{
|
||||
"item_code": ctx.item_code,
|
||||
|
||||
@@ -1080,6 +1080,7 @@ class SerialBatchCreation:
|
||||
def set_serial_batch_entries(self, doc):
|
||||
incoming_rate = self.get("incoming_rate")
|
||||
|
||||
precision = frappe.get_precision("Serial and Batch Entry", "qty")
|
||||
if self.get("serial_nos"):
|
||||
serial_no_wise_batch = frappe._dict({})
|
||||
if self.has_batch_no:
|
||||
@@ -1109,7 +1110,8 @@ class SerialBatchCreation:
|
||||
"entries",
|
||||
{
|
||||
"batch_no": batch_no,
|
||||
"qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
|
||||
"qty": flt(batch_qty, precision)
|
||||
* (-1 if self.type_of_transaction == "Outward" else 1),
|
||||
"incoming_rate": incoming_rate,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<p>
|
||||
<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
|
||||
class="btn btn-primary btn-sm" id="pay-for-order">
|
||||
{{ _("Pay", null, "Amount") }} {{ pay_amount }}
|
||||
{{ _("Pay", null, "Amount") }} {{doc.get_formatted("grand_total") }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -72,8 +72,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-right col-2">
|
||||
{%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase
|
||||
Order'] else doc.customer_name %}
|
||||
{%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
|
||||
<b>{{ party_name }}</b>
|
||||
|
||||
{% if doc.contact_display and doc.contact_display != party_name %}
|
||||
|
||||
Reference in New Issue
Block a user