mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-15 04:45:09 +00:00
Merge pull request #53576 from frappe/version-16-hotfix
This commit is contained in:
@@ -5,8 +5,10 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import cint, flt, fmt_money, get_link_to_form, getdate
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.utils import cint, flt, fmt_money, getdate
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
@@ -182,65 +184,162 @@ def get_payment_entries_for_bank_clearance(
|
||||
):
|
||||
entries = []
|
||||
|
||||
condition = ""
|
||||
pe_condition = ""
|
||||
journal_entry = frappe.qb.DocType("Journal Entry")
|
||||
journal_entry_account = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
journal_entry_query = (
|
||||
frappe.qb.from_(journal_entry_account)
|
||||
.inner_join(journal_entry)
|
||||
.on(journal_entry_account.parent == journal_entry.name)
|
||||
.select(
|
||||
ConstantColumn("Journal Entry").as_("payment_document"),
|
||||
journal_entry.name.as_("payment_entry"),
|
||||
journal_entry.cheque_no.as_("cheque_number"),
|
||||
journal_entry.cheque_date,
|
||||
Sum(journal_entry_account.debit_in_account_currency).as_("debit"),
|
||||
Sum(journal_entry_account.credit_in_account_currency).as_("credit"),
|
||||
journal_entry.posting_date,
|
||||
journal_entry_account.against_account,
|
||||
journal_entry.clearance_date,
|
||||
journal_entry_account.account_currency,
|
||||
)
|
||||
.where(
|
||||
(journal_entry_account.account == account)
|
||||
& (journal_entry.docstatus == 1)
|
||||
& (journal_entry.posting_date >= from_date)
|
||||
& (journal_entry.posting_date <= to_date)
|
||||
& (journal_entry.is_opening == "No")
|
||||
)
|
||||
)
|
||||
|
||||
if not include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
|
||||
journal_entry_query = journal_entry_query.where(
|
||||
(journal_entry.clearance_date.isnull()) | (journal_entry.clearance_date == "0000-00-00")
|
||||
)
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Journal Entry" as payment_document, t1.name as payment_entry,
|
||||
t1.cheque_no as cheque_number, t1.cheque_date,
|
||||
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
|
||||
t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
|
||||
from
|
||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where
|
||||
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
|
||||
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
|
||||
and ifnull(t1.is_opening, 'No') = 'No' {condition}
|
||||
group by t2.account, t1.name
|
||||
order by t1.posting_date ASC, t1.name DESC
|
||||
""",
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
journal_entries = (
|
||||
journal_entry_query.groupby(journal_entry_account.account, journal_entry.name)
|
||||
.orderby(journal_entry.posting_date)
|
||||
.orderby(journal_entry.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
company = frappe.qb.DocType("Company")
|
||||
payment_entry_query = (
|
||||
frappe.qb.from_(pe)
|
||||
.join(company)
|
||||
.on(pe.company == company.name)
|
||||
.select(
|
||||
ConstantColumn("Payment Entry").as_("payment_document"),
|
||||
pe.name.as_("payment_entry"),
|
||||
pe.reference_no.as_("cheque_number"),
|
||||
pe.reference_date.as_("cheque_date"),
|
||||
(
|
||||
Case()
|
||||
.when(
|
||||
pe.paid_from == account,
|
||||
(
|
||||
pe.paid_amount
|
||||
+ (
|
||||
Case()
|
||||
.when(
|
||||
(pe.payment_type == "Pay")
|
||||
& (company.default_currency == pe.paid_from_account_currency),
|
||||
pe.base_total_taxes_and_charges,
|
||||
)
|
||||
.else_(pe.total_taxes_and_charges)
|
||||
)
|
||||
),
|
||||
)
|
||||
.else_(0)
|
||||
).as_("credit"),
|
||||
(
|
||||
Case()
|
||||
.when(pe.paid_from == account, 0)
|
||||
.else_(
|
||||
pe.received_amount
|
||||
+ (
|
||||
Case()
|
||||
.when(
|
||||
company.default_currency == pe.paid_to_account_currency,
|
||||
pe.base_total_taxes_and_charges,
|
||||
)
|
||||
.else_(pe.total_taxes_and_charges)
|
||||
)
|
||||
)
|
||||
).as_("debit"),
|
||||
pe.posting_date,
|
||||
Coalesce(pe.party, Case().when(pe.paid_from == account, pe.paid_to).else_(pe.paid_from)).as_(
|
||||
"against_account"
|
||||
),
|
||||
pe.clearance_date,
|
||||
(
|
||||
Case()
|
||||
.when(pe.paid_to == account, pe.paid_to_account_currency)
|
||||
.else_(pe.paid_from_account_currency)
|
||||
).as_("account_currency"),
|
||||
)
|
||||
.where(
|
||||
((pe.paid_from == account) | (pe.paid_to == account))
|
||||
& (pe.docstatus == 1)
|
||||
& (pe.posting_date >= from_date)
|
||||
& (pe.posting_date <= to_date)
|
||||
)
|
||||
)
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Payment Entry" as payment_document, pe.name as payment_entry,
|
||||
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
|
||||
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
|
||||
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
|
||||
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
|
||||
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry` as pe
|
||||
join `tabCompany` c on c.name = pe.company
|
||||
where
|
||||
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
|
||||
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
|
||||
{pe_condition}
|
||||
order by
|
||||
pe.posting_date ASC, pe.name DESC
|
||||
""",
|
||||
{
|
||||
"account": account,
|
||||
"from": from_date,
|
||||
"to": to_date,
|
||||
},
|
||||
as_dict=1,
|
||||
if not include_reconciled_entries:
|
||||
payment_entry_query = payment_entry_query.where(
|
||||
(pe.clearance_date.isnull()) | (pe.clearance_date == "0000-00-00")
|
||||
)
|
||||
|
||||
payment_entries = (payment_entry_query.orderby(pe.posting_date).orderby(pe.name, order=Order.desc)).run(
|
||||
as_dict=True
|
||||
)
|
||||
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
paid_purchase_invoices_query = (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(acc)
|
||||
.on(pi.cash_bank_account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.paid_amount.as_("credit"),
|
||||
pi.posting_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
pi.bill_no.as_("cheque_number"),
|
||||
pi.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("debit"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.is_paid == 1)
|
||||
& (pi.cash_bank_account == account)
|
||||
& (pi.posting_date >= from_date)
|
||||
& (pi.posting_date <= to_date)
|
||||
)
|
||||
)
|
||||
|
||||
if not include_reconciled_entries:
|
||||
paid_purchase_invoices_query = paid_purchase_invoices_query.where(
|
||||
(pi.clearance_date.isnull()) | (pi.clearance_date == "0000-00-00")
|
||||
)
|
||||
|
||||
paid_purchase_invoices = (
|
||||
paid_purchase_invoices_query.orderby(pi.posting_date).orderby(pi.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
pos_sales_invoices = []
|
||||
|
||||
if include_pos_transactions:
|
||||
si_payment = frappe.qb.DocType("Sales Invoice Payment")
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
pos_sales_invoices = (
|
||||
pos_sales_invoices_query = (
|
||||
frappe.qb.from_(si_payment)
|
||||
.inner_join(si)
|
||||
.on(si_payment.parent == si.name)
|
||||
@@ -263,38 +362,22 @@ def get_payment_entries_for_bank_clearance(
|
||||
& (si.posting_date >= from_date)
|
||||
& (si.posting_date <= to_date)
|
||||
)
|
||||
.orderby(si.posting_date)
|
||||
.orderby(si.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
)
|
||||
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
if not include_reconciled_entries:
|
||||
pos_sales_invoices_query = pos_sales_invoices_query.where(
|
||||
(si_payment.clearance_date.isnull()) | (si_payment.clearance_date == "0000-00-00")
|
||||
)
|
||||
|
||||
pos_purchase_invoices = (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(acc)
|
||||
.on(pi.cash_bank_account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.paid_amount.as_("credit"),
|
||||
pi.posting_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
pi.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("debit"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.cash_bank_account == account)
|
||||
& (pi.posting_date >= from_date)
|
||||
& (pi.posting_date <= to_date)
|
||||
)
|
||||
.orderby(pi.posting_date)
|
||||
.orderby(pi.name, order=Order.desc)
|
||||
pos_sales_invoices = (
|
||||
pos_sales_invoices_query.orderby(si.posting_date).orderby(si.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
entries = (
|
||||
list(payment_entries) + list(journal_entries) + list(pos_sales_invoices) + list(pos_purchase_invoices)
|
||||
list(payment_entries)
|
||||
+ list(journal_entries)
|
||||
+ list(pos_sales_invoices)
|
||||
+ list(paid_purchase_invoices)
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
@@ -101,11 +101,11 @@
|
||||
"label": "Use HTTP Protocol"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:19:02.873815",
|
||||
"modified": "2026-03-16 13:28:21.075743",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
|
||||
@@ -293,6 +293,8 @@ class JournalEntry(AccountsController):
|
||||
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super().on_cancel()
|
||||
|
||||
from_doc_events = getattr(self, "ignore_linked_doctypes", ())
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
@@ -306,6 +308,10 @@ class JournalEntry(AccountsController):
|
||||
"Advance Payment Ledger Entry",
|
||||
"Tax Withholding Entry",
|
||||
)
|
||||
|
||||
if from_doc_events and from_doc_events != self.ignore_linked_doctypes:
|
||||
self.ignore_linked_doctypes = self.ignore_linked_doctypes + from_doc_events
|
||||
|
||||
self.make_gl_entries(1)
|
||||
JournalTaxWithholding(self).on_cancel()
|
||||
self.unlink_advance_entry_reference()
|
||||
|
||||
@@ -2556,14 +2556,9 @@ def get_orders_to_be_billed(
|
||||
if not voucher_type:
|
||||
return []
|
||||
|
||||
# Add cost center condition
|
||||
doc = frappe.get_doc({"doctype": voucher_type})
|
||||
condition = ""
|
||||
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
||||
condition = " and cost_center='%s'" % cost_center
|
||||
|
||||
# dynamic dimension filters
|
||||
active_dimensions = get_dimensions()[0]
|
||||
condition = ""
|
||||
active_dimensions = get_dimensions(True)[0]
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -59,7 +60,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-13 06:52:46.130142",
|
||||
"modified": "2026-03-11 14:26:11.312950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
|
||||
@@ -50,10 +50,10 @@
|
||||
"options": "1"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-09 17:30:41.476806",
|
||||
"modified": "2026-03-16 13:28:19.677217",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Settings",
|
||||
|
||||
@@ -321,7 +321,7 @@
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Date",
|
||||
"label": "Posting Date",
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
"print_hide": 1,
|
||||
@@ -391,7 +391,7 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "bill_no",
|
||||
"collapsible_depends_on": "posting_date",
|
||||
"fieldname": "supplier_invoice_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Supplier Invoice"
|
||||
@@ -1693,7 +1693,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-09 17:15:27.014131",
|
||||
"modified": "2026-03-17 20:44:00.221219",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:19:08.888368",
|
||||
"modified": "2026-03-16 13:28:21.312607",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Settings",
|
||||
|
||||
@@ -383,7 +383,7 @@
|
||||
"hide_seconds": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Date",
|
||||
"label": "Posting Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
|
||||
@@ -919,11 +919,9 @@ class SalesInvoice(SellingController):
|
||||
if self.pos_profile:
|
||||
pos = frappe.get_doc("POS Profile", self.pos_profile)
|
||||
|
||||
if not self.get("payments") and not for_validate:
|
||||
update_multi_mode_option(self, pos)
|
||||
|
||||
if pos:
|
||||
if not for_validate:
|
||||
update_multi_mode_option(self, pos)
|
||||
self.tax_category = pos.get("tax_category")
|
||||
|
||||
if not for_validate and not self.customer:
|
||||
@@ -3008,6 +3006,8 @@ def update_multi_mode_option(doc, pos_profile):
|
||||
payment.account = payment_mode.default_account
|
||||
payment.type = payment_mode.type
|
||||
|
||||
mop_refetched = bool(doc.payments) and not doc.is_created_using_pos
|
||||
|
||||
doc.set("payments", [])
|
||||
invalid_modes = []
|
||||
mode_of_payments = [d.mode_of_payment for d in pos_profile.get("payments")]
|
||||
@@ -3029,6 +3029,12 @@ def update_multi_mode_option(doc, pos_profile):
|
||||
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
|
||||
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
|
||||
|
||||
if mop_refetched:
|
||||
frappe.toast(
|
||||
_("Payment methods refreshed. Please review before proceeding."),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
|
||||
def get_all_mode_of_payments(doc):
|
||||
return frappe.db.sql(
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:18:34.671062",
|
||||
"modified": "2026-03-16 13:28:20.485964",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Settings",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"align_labels_right": 0,
|
||||
"creation": "2016-05-05 17:16:18.564460",
|
||||
"custom_format": 1,
|
||||
"disabled": 0,
|
||||
"doc_type": "Sales Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "Default",
|
||||
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Monospace;\n\t\tline-height: 200%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n<p class=\"text-center\">\n\t{{ company }}<br>\n\t{{ __(\"POS No : \") }} {{ offline_pos_name }}<br>\n</p>\n<p>\n\t<b>{{ __(\"Customer\") }}:</b> {{ customer }}<br>\n</p>\n\n<p>\n\t<b>{{ __(\"Date\") }}:</b> {{ dateutil.global_date_format(posting_date) }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ __(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ __(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ __(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{% for item in items %}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_name }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ format_number(item.qty, null,precision(\"difference\")) }}<br>@ {{ format_currency(item.rate, currency) }}</td>\n\t\t\t<td class=\"text-right\">{{ format_currency(item.amount, currency) }}</td>\n\t\t</tr>\n\t\t{% endfor %}\n\t</tbody>\n</table>\n\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t{{ __(\"Net Total\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(total, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% for row in taxes %}\n\t\t{% if not row.included_in_print_rate %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t{{ row.description }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(row.tax_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% endif %}\n\t\t{% endfor %}\n\t\t{% if discount_amount %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ __(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(discount_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% endif %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(grand_total, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(paid_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Qty Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ qty_total }}\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>\n\n\n<hr>\n<p>{{ terms }}</p>\n<p class=\"text-center\">{{ __(\"Thank you, please visit again.\") }}</p>",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"modified": "2019-09-05 17:20:30.726659",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Point of Sale",
|
||||
"owner": "Administrator",
|
||||
"print_format_builder": 0,
|
||||
"print_format_type": "JS",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import getdate, nowdate
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import getdate
|
||||
from pypika import Order
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -48,17 +51,6 @@ def get_columns():
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = ""
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions += " and posting_date>=%(from_date)s"
|
||||
if filters.get("to_date"):
|
||||
conditions += " and posting_date<=%(to_date)s"
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_entries(filters):
|
||||
entries = []
|
||||
|
||||
@@ -73,41 +65,90 @@ def get_entries(filters):
|
||||
|
||||
return sorted(
|
||||
entries,
|
||||
key=lambda k: k[2].strftime("%H%M%S") or getdate(nowdate()),
|
||||
key=lambda k: getdate(k[2]),
|
||||
)
|
||||
|
||||
|
||||
def get_entries_for_bank_clearance_summary(filters):
|
||||
entries = []
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""SELECT
|
||||
"Journal Entry", jv.name, jv.posting_date, jv.cheque_no,
|
||||
jv.clearance_date, jvd.against_account, jvd.debit - jvd.credit
|
||||
FROM
|
||||
`tabJournal Entry Account` jvd, `tabJournal Entry` jv
|
||||
WHERE
|
||||
jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {conditions}
|
||||
order by posting_date DESC, jv.name DESC""",
|
||||
filters,
|
||||
as_list=1,
|
||||
)
|
||||
journal_entries = (
|
||||
frappe.qb.from_(jea)
|
||||
.inner_join(je)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
ConstantColumn("Journal Entry").as_("payment_document"),
|
||||
je.name.as_("payment_entry"),
|
||||
je.posting_date,
|
||||
je.cheque_no,
|
||||
je.clearance_date,
|
||||
jea.against_account,
|
||||
jea.debit_in_account_currency - jea.credit_in_account_currency,
|
||||
)
|
||||
.where(
|
||||
(jea.account == filters.account)
|
||||
& (je.docstatus == 1)
|
||||
& (je.posting_date >= filters.from_date)
|
||||
& (je.posting_date <= filters.to_date)
|
||||
& ((je.is_opening == "No") | (je.is_opening.isnull()))
|
||||
)
|
||||
.orderby(je.posting_date, order=Order.desc)
|
||||
.orderby(je.name, order=Order.desc)
|
||||
).run(as_list=True)
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""SELECT
|
||||
"Payment Entry", name, posting_date, reference_no, clearance_date, party,
|
||||
if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount)
|
||||
FROM
|
||||
`tabPayment Entry`
|
||||
WHERE
|
||||
docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {conditions}
|
||||
order by posting_date DESC, name DESC""",
|
||||
filters,
|
||||
as_list=1,
|
||||
)
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
payment_entries = (
|
||||
frappe.qb.from_(pe)
|
||||
.select(
|
||||
ConstantColumn("Payment Entry").as_("payment_document"),
|
||||
pe.name.as_("payment_entry"),
|
||||
pe.posting_date,
|
||||
pe.reference_no.as_("cheque_no"),
|
||||
pe.clearance_date,
|
||||
pe.party.as_("against_account"),
|
||||
Case()
|
||||
.when(
|
||||
(pe.paid_from == filters.account),
|
||||
((pe.paid_amount * -1) - pe.total_taxes_and_charges),
|
||||
)
|
||||
.else_(pe.received_amount),
|
||||
)
|
||||
.where((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
|
||||
.where(
|
||||
(pe.docstatus == 1)
|
||||
& (pe.posting_date >= filters.from_date)
|
||||
& (pe.posting_date <= filters.to_date)
|
||||
)
|
||||
.orderby(pe.posting_date, order=Order.desc)
|
||||
.orderby(pe.name, order=Order.desc)
|
||||
).run(as_list=True)
|
||||
|
||||
entries = journal_entries + payment_entries
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
purchase_invoices = (
|
||||
frappe.qb.from_(pi)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.posting_date,
|
||||
pi.bill_no.as_("cheque_no"),
|
||||
pi.clearance_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
(pi.paid_amount * -1).as_("amount"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.is_paid == 1)
|
||||
& (pi.cash_bank_account == filters.account)
|
||||
& (pi.posting_date >= filters.from_date)
|
||||
& (pi.posting_date <= filters.to_date)
|
||||
)
|
||||
.orderby(pi.posting_date, order=Order.desc)
|
||||
.orderby(pi.name, order=Order.desc)
|
||||
).run(as_list=True)
|
||||
|
||||
entries = journal_entries + payment_entries + purchase_invoices
|
||||
|
||||
return entries
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.utils import flt, getdate
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
|
||||
@@ -123,73 +127,143 @@ def get_entries_for_bank_reconciliation_statement(filters):
|
||||
|
||||
payment_entries = get_payment_entries(filters)
|
||||
|
||||
purchase_invoices = get_purchase_invoices(filters)
|
||||
|
||||
pos_entries = []
|
||||
if filters.include_pos_transactions:
|
||||
pos_entries = get_pos_entries(filters)
|
||||
|
||||
return list(journal_entries) + list(payment_entries) + list(pos_entries)
|
||||
return list(journal_entries) + list(payment_entries) + list(pos_entries) + list(purchase_invoices)
|
||||
|
||||
|
||||
def get_journal_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select "Journal Entry" as payment_document, jv.posting_date,
|
||||
jv.name as payment_entry, jvd.debit_in_account_currency as debit,
|
||||
jvd.credit_in_account_currency as credit, jvd.against_account,
|
||||
jv.cheque_no as reference_no, jv.cheque_date as ref_date, jv.clearance_date, jvd.account_currency
|
||||
from
|
||||
`tabJournal Entry Account` jvd, `tabJournal Entry` jv
|
||||
where jvd.parent = jv.name and jv.docstatus=1
|
||||
and jvd.account = %(account)s and jv.posting_date <= %(report_date)s
|
||||
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
|
||||
and ifnull(jv.is_opening, 'No') = 'No'
|
||||
and jv.company = %(company)s """,
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
return (
|
||||
frappe.qb.from_(jea)
|
||||
.join(je)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
ConstantColumn("Journal Entry").as_("payment_document"),
|
||||
je.name.as_("payment_entry"),
|
||||
je.posting_date,
|
||||
jea.debit_in_account_currency.as_("debit"),
|
||||
jea.credit_in_account_currency.as_("credit"),
|
||||
jea.against_account,
|
||||
je.cheque_no.as_("reference_no"),
|
||||
je.cheque_date.as_("ref_date"),
|
||||
je.clearance_date,
|
||||
jea.account_currency,
|
||||
)
|
||||
.where(
|
||||
(je.docstatus == 1)
|
||||
& (jea.account == filters.account)
|
||||
& (je.posting_date <= filters.report_date)
|
||||
& (je.clearance_date.isnull() | (je.clearance_date > filters.report_date))
|
||||
& (je.company == filters.company)
|
||||
& ((je.is_opening.isnull()) | (je.is_opening == "No"))
|
||||
)
|
||||
.orderby(je.posting_date)
|
||||
.orderby(je.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def get_payment_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no, reference_date as ref_date,
|
||||
if(paid_to=%(account)s, received_amount_after_tax, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
where
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date <= %(report_date)s
|
||||
and ifnull(clearance_date, '4000-01-01') > %(report_date)s
|
||||
and company = %(company)s
|
||||
""",
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
return (
|
||||
frappe.qb.from_(pe)
|
||||
.select(
|
||||
ConstantColumn("Payment Entry").as_("payment_document"),
|
||||
pe.name.as_("payment_entry"),
|
||||
pe.reference_no.as_("reference_no"),
|
||||
pe.reference_date.as_("ref_date"),
|
||||
Case().when(pe.paid_to == filters.account, pe.received_amount_after_tax).else_(0).as_("debit"),
|
||||
Case().when(pe.paid_from == filters.account, pe.paid_amount_after_tax).else_(0).as_("credit"),
|
||||
pe.posting_date,
|
||||
Coalesce(
|
||||
pe.party, Case().when(pe.paid_from == filters.account, pe.paid_to).else_(pe.paid_from)
|
||||
).as_("against_account"),
|
||||
pe.clearance_date,
|
||||
(
|
||||
Case()
|
||||
.when(pe.paid_to == filters.account, pe.paid_to_account_currency)
|
||||
.else_(pe.paid_from_account_currency)
|
||||
).as_("account_currency"),
|
||||
)
|
||||
.where(
|
||||
(pe.docstatus == 1)
|
||||
& ((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
|
||||
& (pe.posting_date <= filters.report_date)
|
||||
& (pe.clearance_date.isnull() | (pe.clearance_date > filters.report_date))
|
||||
& (pe.company == filters.company)
|
||||
)
|
||||
.orderby(pe.posting_date)
|
||||
.orderby(pe.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def get_purchase_invoices(filters):
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
return (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(acc)
|
||||
.on(pi.cash_bank_account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.bill_no.as_("reference_no"),
|
||||
pi.posting_date.as_("ref_date"),
|
||||
Case().when(pi.paid_amount < 0, pi.paid_amount * -1).else_(0).as_("debit"),
|
||||
Case().when(pi.paid_amount > 0, pi.paid_amount).else_(0).as_("credit"),
|
||||
pi.posting_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
pi.clearance_date,
|
||||
acc.account_currency,
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.is_paid == 1)
|
||||
& (pi.cash_bank_account == filters.account)
|
||||
& (pi.posting_date <= filters.report_date)
|
||||
& (pi.clearance_date.isnull() | (pi.clearance_date > filters.report_date))
|
||||
& (pi.company == filters.company)
|
||||
)
|
||||
.orderby(pi.posting_date)
|
||||
.orderby(pi.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def get_pos_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
|
||||
si.posting_date, si.debit_to as against_account, sip.clearance_date,
|
||||
account.account_currency, 0 as credit
|
||||
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
|
||||
where
|
||||
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
|
||||
and account.name = sip.account and si.posting_date <= %(report_date)s and
|
||||
ifnull(sip.clearance_date, '4000-01-01') > %(report_date)s
|
||||
and si.company = %(company)s
|
||||
order by
|
||||
si.posting_date ASC, si.name DESC
|
||||
""",
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
si_payment = frappe.qb.DocType("Sales Invoice Payment")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
return (
|
||||
frappe.qb.from_(si_payment)
|
||||
.join(si)
|
||||
.on(si_payment.parent == si.name)
|
||||
.join(acc)
|
||||
.on(si_payment.account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Sales Invoice").as_("payment_document"),
|
||||
si.name.as_("payment_entry"),
|
||||
si_payment.amount.as_("debit"),
|
||||
si.posting_date,
|
||||
si.debit_to.as_("against_account"),
|
||||
si_payment.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("credit"),
|
||||
)
|
||||
.where(
|
||||
(si_payment.account == filters.account)
|
||||
& (si.docstatus == 1)
|
||||
& (si.posting_date <= filters.report_date)
|
||||
& (si_payment.clearance_date.isnull() | (si_payment.clearance_date > filters.report_date))
|
||||
& (si.company == filters.company)
|
||||
)
|
||||
.orderby(si.posting_date)
|
||||
.orderby(si_payment.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def get_amounts_not_reflected_in_system(filters):
|
||||
@@ -205,30 +279,66 @@ def get_amounts_not_reflected_in_system(filters):
|
||||
|
||||
|
||||
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
|
||||
je_amount = frappe.db.sql(
|
||||
"""
|
||||
select sum(jvd.debit_in_account_currency - jvd.credit_in_account_currency)
|
||||
from `tabJournal Entry Account` jvd, `tabJournal Entry` jv
|
||||
where jvd.parent = jv.name and jv.docstatus=1 and jvd.account=%(account)s
|
||||
and jv.posting_date > %(report_date)s and jv.clearance_date <= %(report_date)s
|
||||
and ifnull(jv.is_opening, 'No') = 'No' """,
|
||||
filters,
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
je_amount = (
|
||||
frappe.qb.from_(jea)
|
||||
.inner_join(je)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("amount"),
|
||||
)
|
||||
.where(
|
||||
(je.docstatus == 1)
|
||||
& (jea.account == filters.account)
|
||||
& (je.posting_date > filters.report_date)
|
||||
& (je.clearance_date <= filters.report_date)
|
||||
& (je.company == filters.company)
|
||||
& ((je.is_opening.isnull()) | (je.is_opening == "No"))
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
je_amount = flt(je_amount[0].amount) if je_amount else 0.0
|
||||
|
||||
je_amount = flt(je_amount[0][0]) if je_amount else 0.0
|
||||
|
||||
pe_amount = frappe.db.sql(
|
||||
"""
|
||||
select sum(if(paid_from=%(account)s, paid_amount, received_amount))
|
||||
from `tabPayment Entry`
|
||||
where (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date > %(report_date)s and clearance_date <= %(report_date)s""",
|
||||
filters,
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
pe_amount = (
|
||||
frappe.qb.from_(pe)
|
||||
.select(
|
||||
Sum(Case().when(pe.paid_from == filters.account, pe.paid_amount).else_(pe.received_amount)).as_(
|
||||
"amount"
|
||||
),
|
||||
)
|
||||
.where(
|
||||
((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
|
||||
& (pe.docstatus == 1)
|
||||
& (pe.posting_date > filters.report_date)
|
||||
& (pe.clearance_date <= filters.report_date)
|
||||
& (pe.company == filters.company)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
pe_amount = flt(pe_amount[0].amount) if pe_amount else 0.0
|
||||
|
||||
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
pi_amount = (
|
||||
frappe.qb.from_(pi)
|
||||
.select(
|
||||
Sum(pi.paid_amount).as_("amount"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.is_paid == 1)
|
||||
& (pi.cash_bank_account == filters.account)
|
||||
& (pi.posting_date > filters.report_date)
|
||||
& (pi.clearance_date <= filters.report_date)
|
||||
& (pi.company == filters.company)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return je_amount + pe_amount
|
||||
pi_amount = flt(pi_amount[0].amount) if pi_amount else 0.0
|
||||
|
||||
return je_amount + pe_amount + pi_amount
|
||||
|
||||
|
||||
def get_balance_row(label, amount, account_currency):
|
||||
|
||||
@@ -32,6 +32,7 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
|
||||
item_list = get_items(filters, additional_table_columns)
|
||||
aii_account_map = get_aii_accounts()
|
||||
default_taxes = {}
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(
|
||||
item_list,
|
||||
@@ -40,6 +41,9 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
doctype="Purchase Invoice",
|
||||
tax_doctype="Purchase Taxes and Charges",
|
||||
)
|
||||
for tax in tax_columns:
|
||||
default_taxes[f"{tax}_rate"] = 0
|
||||
default_taxes[f"{tax}_amount"] = 0
|
||||
|
||||
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
|
||||
|
||||
@@ -87,6 +91,7 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
|
||||
total_tax = 0
|
||||
total_other_charges = 0
|
||||
row.update(default_taxes.copy())
|
||||
for tax, details in itemised_tax.get(d.name, {}).items():
|
||||
row.update(
|
||||
{
|
||||
|
||||
@@ -33,6 +33,10 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
return columns, [], None, None, None, 0
|
||||
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
default_taxes = {}
|
||||
for tax in tax_columns:
|
||||
default_taxes[f"{tax}_rate"] = 0
|
||||
default_taxes[f"{tax}_amount"] = 0
|
||||
|
||||
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
|
||||
so_dn_map = get_delivery_notes_against_sales_order(item_list)
|
||||
@@ -90,6 +94,9 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
|
||||
total_tax = 0
|
||||
total_other_charges = 0
|
||||
|
||||
row.update(default_taxes.copy())
|
||||
|
||||
for tax, details in itemised_tax.get(d.name, {}).items():
|
||||
row.update(
|
||||
{
|
||||
|
||||
@@ -16,9 +16,11 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
item=item or self.item,
|
||||
item_name=item or self.item,
|
||||
description=item or self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
@@ -29,6 +31,19 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase):
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
for tax in taxes or []:
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": tax["account_head"],
|
||||
"cost_center": self.cost_center,
|
||||
"description": tax["description"],
|
||||
"rate": tax["rate"],
|
||||
},
|
||||
)
|
||||
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
@@ -62,3 +77,50 @@ class TestItemWiseSalesRegister(AccountsTestMixin, IntegrationTestCase):
|
||||
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
|
||||
def test_grouped_report_handles_different_tax_descriptions(self):
|
||||
self.create_item(item_name="_Test Item Tax Description A")
|
||||
first_item = self.item
|
||||
self.create_item(item_name="_Test Item Tax Description B")
|
||||
second_item = self.item
|
||||
|
||||
first_tax_description = "Tax Description A"
|
||||
second_tax_description = "Tax Description B"
|
||||
first_tax_amount_field = f"{frappe.scrub(first_tax_description)}_amount"
|
||||
second_tax_amount_field = f"{frappe.scrub(second_tax_description)}_amount"
|
||||
|
||||
self.create_sales_invoice(
|
||||
item=first_item,
|
||||
taxes=[
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": first_tax_description,
|
||||
"rate": 5,
|
||||
}
|
||||
],
|
||||
)
|
||||
self.create_sales_invoice(
|
||||
item=second_item,
|
||||
taxes=[
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"description": second_tax_description,
|
||||
"rate": 2,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"company": self.company,
|
||||
"group_by": "Customer",
|
||||
}
|
||||
)
|
||||
_, data, _, _, _, _ = execute(filters)
|
||||
|
||||
grand_total_row = next(row for row in data if row.get("bold") and row.get("item_code") == "Total")
|
||||
|
||||
self.assertEqual(grand_total_row[first_tax_amount_field], 5.0)
|
||||
self.assertEqual(grand_total_row[second_tax_amount_field], 2.0)
|
||||
|
||||
@@ -39,7 +39,7 @@ frappe.query_reports[PL_REPORT_NAME]["filters"].push(
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"idx": 4,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:49.950442",
|
||||
"letter_head": null,
|
||||
"modified": "2026-03-13 17:35:39.703838",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Trends",
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.070651",
|
||||
"letter_head": null,
|
||||
"modified": "2026-03-13 17:36:13.725601",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Trends",
|
||||
|
||||
@@ -1536,7 +1536,7 @@ def parse_naming_series_variable(doc, variable):
|
||||
getdate(doc.get("posting_date") or doc.get("transaction_date") or doc.get("posting_datetime"))
|
||||
or now_datetime()
|
||||
)
|
||||
if frappe.get_single_value("Global Defaults", "use_posting_datetime_for_naming_documents")
|
||||
if doc and frappe.get_single_value("Global Defaults", "use_posting_datetime_for_naming_documents")
|
||||
else now_datetime()
|
||||
)
|
||||
return date.strftime(data[variable]) if variable in data else determine_consecutive_week_number(date)
|
||||
|
||||
@@ -36,6 +36,7 @@ frappe.ui.form.on("Asset", {
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
frm.trigger("set_dynamic_labels");
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
},
|
||||
|
||||
@@ -87,6 +88,8 @@ frappe.ui.form.on("Asset", {
|
||||
},
|
||||
|
||||
refresh: async function (frm) {
|
||||
frm.trigger("set_dynamic_labels");
|
||||
|
||||
frappe.ui.form.trigger("Asset", "asset_type");
|
||||
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
|
||||
|
||||
@@ -227,6 +230,10 @@ frappe.ui.form.on("Asset", {
|
||||
}
|
||||
},
|
||||
|
||||
set_dynamic_labels: function (frm) {
|
||||
frm.set_currency_labels(["net_purchase_amount"], erpnext.get_currency(frm.doc.company));
|
||||
},
|
||||
|
||||
should_show_accounting_ledger: async function (frm) {
|
||||
if (["Capitalized"].includes(frm.doc.status)) {
|
||||
return false;
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"company",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"asset_name",
|
||||
"location",
|
||||
"image",
|
||||
"column_break_3",
|
||||
"location",
|
||||
"company",
|
||||
"asset_category",
|
||||
"asset_type",
|
||||
"maintenance_required",
|
||||
@@ -533,7 +533,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Purchase Amount",
|
||||
"mandatory_depends_on": "eval:(doc.asset_type != \"Composite Asset\" || doc.docstatus==1)",
|
||||
"options": "Company:company:default_currency",
|
||||
"options": "currency",
|
||||
"read_only_depends_on": "eval: doc.asset_type == \"Composite Asset\""
|
||||
},
|
||||
{
|
||||
@@ -626,7 +626,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2026-03-09 17:15:32.819896",
|
||||
"modified": "2026-03-12 16:07:39.543227",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -86,6 +86,26 @@ frappe.ui.form.on("Asset Repair", {
|
||||
}
|
||||
},
|
||||
|
||||
show_general_ledger: function (frm) {
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(
|
||||
__("Accounting Ledger"),
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.completion_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
repair_status: (frm) => {
|
||||
if (frm.doc.completion_date && frm.doc.repair_status == "Completed") {
|
||||
frappe.call({
|
||||
@@ -164,26 +184,6 @@ frappe.ui.form.on("Asset Repair Purchase Invoice", {
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
show_general_ledger: (frm) => {
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(
|
||||
__("Accounting Ledger"),
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Asset Repair Consumed Item", {
|
||||
|
||||
@@ -282,13 +282,13 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:16:35.885540",
|
||||
"modified": "2026-03-16 13:28:19.432589",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -67,7 +67,7 @@ frappe.ui.form.on("Purchase Order", {
|
||||
},
|
||||
|
||||
transaction_date(frm) {
|
||||
prevent_past_schedule_dates(frm);
|
||||
erpnext.buying.prevent_past_schedule_dates(frm);
|
||||
frm.set_value("schedule_date", "");
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@ frappe.ui.form.on("Purchase Order", {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
erpnext.set_unit_price_items_note(frm);
|
||||
}
|
||||
prevent_past_schedule_dates(frm);
|
||||
erpnext.buying.prevent_past_schedule_dates(frm);
|
||||
},
|
||||
|
||||
get_materials_from_supplier: function (frm) {
|
||||
@@ -779,11 +779,3 @@ frappe.ui.form.on("Purchase Order", "is_subcontracted", function (frm) {
|
||||
erpnext.buying.get_default_bom(frm);
|
||||
}
|
||||
});
|
||||
|
||||
function prevent_past_schedule_dates(frm) {
|
||||
if (frm.doc.transaction_date) {
|
||||
frm.fields_dict["schedule_date"].datepicker?.update({
|
||||
minDate: new Date(frm.doc.transaction_date),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,14 +165,10 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
},
|
||||
|
||||
show_supplier_quotation_comparison(frm) {
|
||||
const today = new Date();
|
||||
const oneMonthAgo = new Date(today);
|
||||
oneMonthAgo.setMonth(today.getMonth() - 1);
|
||||
|
||||
frappe.route_options = {
|
||||
company: frm.doc.company,
|
||||
from_date: moment(oneMonthAgo).format("YYYY-MM-DD"),
|
||||
to_date: moment(today).format("YYYY-MM-DD"),
|
||||
from_date: moment(frm.doc.transaction_date).format("YYYY-MM-DD"),
|
||||
to_date: moment(new Date()).format("YYYY-MM-DD"),
|
||||
request_for_quotation: frm.doc.name,
|
||||
};
|
||||
frappe.set_route("query-report", "Supplier Quotation Comparison");
|
||||
|
||||
@@ -65,6 +65,11 @@ frappe.query_reports["Purchase Analytics"] = {
|
||||
default: "Monthly",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_aggregate_value_from_subsidiary_companies",
|
||||
label: __("Show Aggregate Value from Subsidiary Companies"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
get_datatable_options(options) {
|
||||
return Object.assign(options, {
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.058154",
|
||||
"letter_head": null,
|
||||
"modified": "2026-03-13 17:36:05.561765",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Trends",
|
||||
|
||||
@@ -1092,12 +1092,9 @@ class BuyingController(SubcontractingController):
|
||||
}
|
||||
)
|
||||
for dimension in accounting_dimensions[0]:
|
||||
asset.update(
|
||||
{
|
||||
dimension["fieldname"]: self.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
fieldname = dimension["fieldname"]
|
||||
default_dimension = accounting_dimensions[1].get(self.company, {}).get(fieldname)
|
||||
asset.update({fieldname: row.get(fieldname) or self.get(fieldname) or default_dimension})
|
||||
|
||||
asset.flags.ignore_validate = True
|
||||
asset.flags.ignore_mandatory = True
|
||||
|
||||
@@ -360,13 +360,13 @@ def copy_attributes_to_variant(item, variant):
|
||||
else:
|
||||
if item.variant_based_on == "Item Attribute":
|
||||
if variant.attributes:
|
||||
attributes_description = item.description + " "
|
||||
attributes_description = item.description or ""
|
||||
for d in variant.attributes:
|
||||
attributes_description += (
|
||||
"<div>" + d.attribute + ": " + cstr(d.attribute_value) + "</div>"
|
||||
)
|
||||
|
||||
if attributes_description not in variant.description:
|
||||
if attributes_description not in (variant.description or ""):
|
||||
variant.description = attributes_description
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from frappe.utils import cint, nowdate, today, unique
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import build_qb_match_conditions
|
||||
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template
|
||||
|
||||
|
||||
@@ -616,34 +617,37 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
|
||||
# income account can be any Credit account,
|
||||
# but can also be a Asset account with account_type='Income Account' in special circumstances.
|
||||
# Hence the first condition is an "OR"
|
||||
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
doctype = "Account"
|
||||
condition = ""
|
||||
dt = "Account"
|
||||
|
||||
acc = qb.DocType(dt)
|
||||
condition = [
|
||||
(acc.report_type.eq("Profit and Loss") | acc.account_type.isin(["Income Account", "Temporary"])),
|
||||
acc.is_group.eq(0),
|
||||
acc.disabled.eq(0),
|
||||
]
|
||||
if txt:
|
||||
condition.append(acc.name.like(f"%{txt}%"))
|
||||
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
condition.append(acc.company.eq(filters.get("company")))
|
||||
|
||||
condition += " and tabAccount.disabled = %(disabled)s"
|
||||
user_perms = build_qb_match_conditions(dt)
|
||||
condition.extend(user_perms)
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""select tabAccount.name from `tabAccount`
|
||||
where (tabAccount.report_type = "Profit and Loss"
|
||||
or tabAccount.account_type in ("Income Account", "Temporary"))
|
||||
and tabAccount.is_group=0
|
||||
and tabAccount.`{searchfield}` LIKE %(txt)s
|
||||
{condition} {get_match_cond(doctype)}
|
||||
order by idx desc, name""",
|
||||
{
|
||||
"txt": "%" + txt + "%",
|
||||
"company": filters.get("company", ""),
|
||||
"disabled": cint(filters.get("disabled", 0)),
|
||||
},
|
||||
return (
|
||||
qb.from_(acc)
|
||||
.select(acc.name)
|
||||
.where(Criterion.all(condition))
|
||||
.orderby(acc.idx, order=Order.desc)
|
||||
.orderby(acc.name)
|
||||
.run()
|
||||
)
|
||||
|
||||
|
||||
@@ -704,26 +708,38 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters,
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
doctype = "Account"
|
||||
condition = ""
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
dt = "Account"
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""select tabAccount.name from `tabAccount`
|
||||
where (tabAccount.report_type = "Profit and Loss"
|
||||
or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed", "Capital Work in Progress"))
|
||||
and tabAccount.is_group=0
|
||||
and tabAccount.disabled = 0
|
||||
and tabAccount.{searchfield} LIKE %(txt)s
|
||||
{condition} {get_match_cond(doctype)}""",
|
||||
{"company": filters.get("company", ""), "txt": "%" + txt + "%"},
|
||||
)
|
||||
acc = qb.DocType(dt)
|
||||
condition = [
|
||||
(
|
||||
acc.report_type.eq("Profit and Loss")
|
||||
| acc.account_type.isin(
|
||||
[
|
||||
"Expense Account",
|
||||
"Fixed Asset",
|
||||
"Temporary",
|
||||
"Asset Received But Not Billed",
|
||||
"Capital Work in Progress",
|
||||
]
|
||||
)
|
||||
),
|
||||
acc.is_group.eq(0),
|
||||
acc.disabled.eq(0),
|
||||
]
|
||||
if txt:
|
||||
condition.append(acc.name.like(f"%{txt}%"))
|
||||
|
||||
if filters.get("company"):
|
||||
condition.append(acc.company.eq(filters.get("company")))
|
||||
|
||||
user_perms = build_qb_match_conditions(dt)
|
||||
condition.extend(user_perms)
|
||||
|
||||
return qb.from_(acc).select(acc.name).where(Criterion.all(condition)).run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -596,7 +596,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
target_doc.against_sales_order = source_doc.against_sales_order
|
||||
target_doc.against_sales_invoice = source_doc.against_sales_invoice
|
||||
target_doc.so_detail = source_doc.so_detail
|
||||
target_doc.si_detail = source_doc.si_detail
|
||||
target_doc.expense_account = source_doc.expense_account
|
||||
target_doc.dn_detail = source_doc.name
|
||||
if default_warehouse_for_sales_return:
|
||||
|
||||
@@ -526,6 +526,9 @@ class SellingController(StockController):
|
||||
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
||||
return
|
||||
|
||||
if self.doctype == "Sales Invoice" and not self.update_stock and not self.is_internal_transfer():
|
||||
return
|
||||
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos
|
||||
|
||||
allow_at_arms_length_price = frappe.get_cached_value(
|
||||
|
||||
@@ -444,7 +444,10 @@ class StatusUpdater(Document):
|
||||
):
|
||||
return
|
||||
|
||||
if args["source_dt"] != "Pick List Item" and args["target_dt"] != "Quotation Item":
|
||||
if args["source_dt"] != "Pick List Item" and args["target_dt"] not in [
|
||||
"Quotation Item",
|
||||
"Packed Item",
|
||||
]:
|
||||
if qty_or_amount == "qty":
|
||||
action_msg = _(
|
||||
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
||||
|
||||
@@ -102,10 +102,10 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:18:46.617101",
|
||||
"modified": "2026-03-16 13:28:21.198138",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Appointment Booking Settings",
|
||||
|
||||
@@ -101,12 +101,12 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-cog",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:18:52.204988",
|
||||
"modified": "2026-03-16 13:28:19.573964",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Settings",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"label": "Banking",
|
||||
"link_to": "Banking",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-01-12 12:29:48.687545",
|
||||
"modified": "2026-02-12 12:29:48.687545",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Banking",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -221,11 +221,12 @@ website_route_rules = [
|
||||
|
||||
standard_navbar_items = [
|
||||
{
|
||||
"item_label": "Clear Demo Data",
|
||||
"item_label": "Delete Demo Data",
|
||||
"item_type": "Action",
|
||||
"action": "erpnext.demo.clear_demo();",
|
||||
"is_standard": 1,
|
||||
"condition": "eval: frappe.boot.sysdefaults.demo_company",
|
||||
"condition": "eval: frappe.boot.sysdefaults.demo_company && frappe.boot.sysdefaults.demo_company.length > 0",
|
||||
"icon": "trash",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -383,9 +383,8 @@ class JobCard(Document):
|
||||
# if key number reaches/crosses to production_capacity means capacity is full and overlap error generated
|
||||
# this will store last to_time of sequential job cards
|
||||
alloted_capacity = {1: time_logs[0]["to_time"]}
|
||||
# flag for sequential Job card found
|
||||
sequential_job_card_found = False
|
||||
for i in range(1, len(time_logs)):
|
||||
sequential_job_card_found = False
|
||||
# scanning for all Existing keys
|
||||
for key in alloted_capacity.keys():
|
||||
# if current Job Card from time is greater than last to_time in that key means these job card are sequential
|
||||
|
||||
@@ -239,12 +239,12 @@
|
||||
"label": "Allow Editing of Items and Quantities in Work Order"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "icon-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:18:05.759229",
|
||||
"modified": "2026-03-16 13:28:20.714576",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
|
||||
@@ -731,6 +731,7 @@ class ProductionPlan(Document):
|
||||
"description": d.description,
|
||||
"stock_uom": d.stock_uom,
|
||||
"company": self.company,
|
||||
"source_warehouse": frappe.get_value("BOM", d.bom_no, "default_source_warehouse"),
|
||||
"fg_warehouse": d.warehouse,
|
||||
"production_plan": self.name,
|
||||
"production_plan_item": d.name,
|
||||
@@ -807,6 +808,7 @@ class ProductionPlan(Document):
|
||||
continue
|
||||
|
||||
work_order_data = {
|
||||
"source_warehouse": frappe.get_value("BOM", row.bom_no, "default_source_warehouse"),
|
||||
"wip_warehouse": default_warehouses.get("wip_warehouse"),
|
||||
"fg_warehouse": default_warehouses.get("fg_warehouse"),
|
||||
"scrap_warehouse": default_warehouses.get("scrap_warehouse"),
|
||||
@@ -1896,7 +1898,7 @@ def get_item_data(item_code):
|
||||
return {
|
||||
"bom_no": item_details.get("bom_no"),
|
||||
"stock_uom": item_details.get("stock_uom"),
|
||||
# "description": item_details.get("description")
|
||||
"description": item_details.get("description"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1912,6 +1914,7 @@ def get_sub_assembly_items(
|
||||
skip_available_sub_assembly_item=False,
|
||||
):
|
||||
data = get_bom_children(parent=bom_no)
|
||||
precision = frappe.get_precision("Production Plan Sub Assembly Item", "qty")
|
||||
for d in data:
|
||||
if d.expandable:
|
||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||
@@ -1951,8 +1954,8 @@ def get_sub_assembly_items(
|
||||
"is_sub_contracted_item": d.is_sub_contracted_item,
|
||||
"bom_level": indent,
|
||||
"indent": indent,
|
||||
"stock_qty": stock_qty,
|
||||
"required_qty": required_qty,
|
||||
"stock_qty": flt(stock_qty, precision),
|
||||
"required_qty": flt(required_qty, precision),
|
||||
"projected_qty": bin_details[d.item_code][0].get("projected_qty", 0)
|
||||
if bin_details.get(d.item_code)
|
||||
else 0,
|
||||
@@ -2098,16 +2101,16 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
|
||||
for item in query.run(as_dict=True):
|
||||
key = (item.item_code, item.bom_no)
|
||||
if item.is_phantom_item:
|
||||
sub_assembly_items[key] += item.get("qty")
|
||||
existing_key = (item.item_code, item.bom_no or item.main_bom)
|
||||
|
||||
if (item.bom_no and key not in sub_assembly_items) or (
|
||||
(item.item_code, item.bom_no or item.main_bom) in existing_sub_assembly_items
|
||||
):
|
||||
if item.bom_no and not item.is_phantom_item and key not in sub_assembly_items:
|
||||
continue
|
||||
|
||||
if not item.is_phantom_item and existing_key in existing_sub_assembly_items:
|
||||
continue
|
||||
|
||||
if item.bom_no:
|
||||
planned_qty = flt(sub_assembly_items[key])
|
||||
recursion_qty = flt(item.get("qty")) if item.is_phantom_item else flt(sub_assembly_items[key])
|
||||
get_raw_materials_of_sub_assembly_items(
|
||||
existing_sub_assembly_items,
|
||||
item_details,
|
||||
@@ -2115,9 +2118,10 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
item.bom_no,
|
||||
include_non_stock_items,
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
planned_qty=recursion_qty,
|
||||
)
|
||||
existing_sub_assembly_items.add((item.item_code, item.bom_no or item.main_bom))
|
||||
if not item.is_phantom_item:
|
||||
existing_sub_assembly_items.add(existing_key)
|
||||
else:
|
||||
if not item.conversion_factor and item.purchase_uom:
|
||||
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
|
||||
|
||||
@@ -2713,6 +2713,92 @@ class TestProductionPlan(IntegrationTestCase):
|
||||
[item.item_code for item in plan.mr_items], ["Item Level 1-3", "Item Level 2-3", "Item Level 3-1"]
|
||||
)
|
||||
|
||||
def test_phantom_bom_explosion_across_multiple_po_items(self):
|
||||
"""
|
||||
Regression: when the same phantom item (BOM) is referenced inside sub-assemblies
|
||||
of two different production plan items, its raw materials must be fully exploded
|
||||
for *both* plan items.
|
||||
"""
|
||||
# Setup items
|
||||
fg_a = make_item("FG for Cross-PO Phantom Test A")
|
||||
fg_b = make_item("FG for Cross-PO Phantom Test B")
|
||||
sa_a = make_item("SA for Cross-PO Phantom Test A")
|
||||
sa_b = make_item("SA for Cross-PO Phantom Test B")
|
||||
phantom = make_item("Phantom for Cross-PO Test")
|
||||
rm = make_item("RM for Cross-PO Phantom Test")
|
||||
|
||||
# Create the shared phantom BOM
|
||||
phantom_bom = make_bom(item=phantom.name, raw_materials=[rm.name], do_not_save=True)
|
||||
phantom_bom.is_phantom_bom = 1
|
||||
phantom_bom.save()
|
||||
phantom_bom.submit()
|
||||
|
||||
# Create SA-A BOM with phantom
|
||||
sa_a_bom = make_bom(item=sa_a.name, raw_materials=[phantom.name], do_not_save=True)
|
||||
sa_a_bom.items[0].bom_no = phantom_bom.name
|
||||
sa_a_bom.save()
|
||||
sa_a_bom.submit()
|
||||
|
||||
# Create SA-B BOM with the SAME phantom
|
||||
sa_b_bom = make_bom(item=sa_b.name, raw_materials=[phantom.name], do_not_save=True)
|
||||
sa_b_bom.items[0].bom_no = phantom_bom.name
|
||||
sa_b_bom.save()
|
||||
sa_b_bom.submit()
|
||||
|
||||
# Create FG-A BOM with SA-A
|
||||
fg_a_bom = make_bom(item=fg_a.name, raw_materials=[sa_a.name], do_not_save=True)
|
||||
fg_a_bom.items[0].bom_no = sa_a_bom.name
|
||||
fg_a_bom.save()
|
||||
fg_a_bom.submit()
|
||||
|
||||
# Create FG-B BOM with SA-B
|
||||
fg_b_bom = make_bom(item=fg_b.name, raw_materials=[sa_b.name], do_not_save=True)
|
||||
fg_b_bom.items[0].bom_no = sa_b_bom.name
|
||||
fg_b_bom.save()
|
||||
fg_b_bom.submit()
|
||||
|
||||
# Build Production Plan with both FGs
|
||||
plan = frappe.new_doc("Production Plan")
|
||||
plan.company = "_Test Company"
|
||||
plan.posting_date = nowdate()
|
||||
plan.ignore_existing_ordered_qty = 1
|
||||
plan.skip_available_sub_assembly_item = 1
|
||||
plan.sub_assembly_warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
for fg_item, bom in [(fg_a.name, fg_a_bom.name), (fg_b.name, fg_b_bom.name)]:
|
||||
plan.append(
|
||||
"po_items",
|
||||
{
|
||||
"use_multi_level_bom": 1,
|
||||
"item_code": fg_item,
|
||||
"bom_no": bom,
|
||||
"planned_qty": 1,
|
||||
"planned_start_date": now_datetime(),
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
)
|
||||
|
||||
plan.insert()
|
||||
plan.get_sub_assembly_items()
|
||||
|
||||
# Verify both sub-assemblies are present
|
||||
sa_items = {row.production_item for row in plan.sub_assembly_items}
|
||||
self.assertIn(sa_a.name, sa_items)
|
||||
self.assertIn(sa_b.name, sa_items)
|
||||
|
||||
plan.submit()
|
||||
|
||||
mr_items = get_items_for_material_requests(plan.as_dict())
|
||||
|
||||
# Phantom raw material should be counted twice (once per FG → SA → shared phantom)
|
||||
rm_total_qty = sum(flt(d["quantity"]) for d in mr_items if d["item_code"] == rm.name)
|
||||
self.assertEqual(
|
||||
rm_total_qty,
|
||||
2.0,
|
||||
f"Expected RM qty=2 (1 per FG via shared phantom BOM), got {rm_total_qty}. "
|
||||
"The phantom BOM was not re-exploded for the second po_item.",
|
||||
)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-11 13:00:09.092676",
|
||||
"modified": "2026-03-16 10:28:41.879801",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
|
||||
@@ -570,7 +570,8 @@
|
||||
{
|
||||
"fieldname": "production_plan_sub_assembly_item",
|
||||
"fieldtype": "Data",
|
||||
"label": "Production Plan Sub-assembly Item",
|
||||
"hidden": 1,
|
||||
"label": "Production Plan Sub Assembly Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -704,7 +705,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-06 17:53:11.295600",
|
||||
"modified": "2026-03-16 10:15:28.708688",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -101,9 +101,6 @@ class Workstation(Document):
|
||||
self.total_working_hours += row.hours
|
||||
|
||||
def validate_working_hours(self, row):
|
||||
if not (row.start_time and row.end_time):
|
||||
frappe.throw(_("Row #{0}: Start Time and End Time are required").format(row.idx))
|
||||
|
||||
if get_time(row.start_time) >= get_time(row.end_time):
|
||||
frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx))
|
||||
|
||||
|
||||
@@ -470,3 +470,5 @@ erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2
|
||||
erpnext.patches.v16_0.migrate_asset_type_checkboxes_to_select
|
||||
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
|
||||
erpnext.patches.v16_0.enable_serial_batch_setting
|
||||
erpnext.patches.v16_0.update_requested_qty_packed_item
|
||||
erpnext.patches.v16_0.remove_payables_receivables_workspace
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
for ws in ["Receivables", "Payables"]:
|
||||
frappe.delete_doc_if_exists("Workspace Sidebar", ws)
|
||||
frappe.delete_doc_if_exists("Workspace", ws)
|
||||
24
erpnext/patches/v16_0/update_requested_qty_packed_item.py
Normal file
24
erpnext/patches/v16_0/update_requested_qty_packed_item.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
|
||||
def execute():
|
||||
MaterialRequestItem = frappe.qb.DocType("Material Request Item")
|
||||
|
||||
mri_query = (
|
||||
frappe.qb.from_(MaterialRequestItem)
|
||||
.select(MaterialRequestItem.packed_item, Sum(MaterialRequestItem.qty))
|
||||
.where((MaterialRequestItem.packed_item.isnotnull()) & (MaterialRequestItem.docstatus == 1))
|
||||
.groupby(MaterialRequestItem.packed_item)
|
||||
)
|
||||
|
||||
mri_data = mri_query.run()
|
||||
|
||||
if not mri_data:
|
||||
return
|
||||
|
||||
updates_against_mr = {data[0]: {"requested_qty": data[1]} for data in mri_data}
|
||||
|
||||
frappe.db.auto_commit_on_many_writes = True
|
||||
frappe.db.bulk_update("Packed Item", updates_against_mr)
|
||||
frappe.db.auto_commit_on_many_writes = False
|
||||
@@ -43,10 +43,10 @@
|
||||
"label": "Fetch Timesheet in Sales Invoice"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:18:28.414222",
|
||||
"modified": "2026-03-16 13:28:20.265634",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Projects Settings",
|
||||
|
||||
@@ -682,3 +682,10 @@ erpnext.buying.get_items_from_product_bundle = function (frm) {
|
||||
|
||||
dialog.show();
|
||||
};
|
||||
erpnext.buying.prevent_past_schedule_dates = function (frm) {
|
||||
if (frm.doc.transaction_date) {
|
||||
frm.fields_dict["schedule_date"].datepicker?.update({
|
||||
minDate: new Date(frm.doc.transaction_date),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ frappe.provide("erpnext.demo");
|
||||
|
||||
$(document).on("desktop_screen", function (event, data) {
|
||||
data.desktop.add_menu_item({
|
||||
label: __("Clear Demo Data"),
|
||||
label: __("Delete Demo Data"),
|
||||
icon: "trash",
|
||||
condition: function () {
|
||||
return frappe.boot.sysdefaults.demo_company;
|
||||
|
||||
@@ -42,9 +42,9 @@ erpnext.landed_cost_taxes_and_charges = {
|
||||
|
||||
if (row.account_currency == company_currency) {
|
||||
row.exchange_rate = 1;
|
||||
frm.set_df_property("taxes", "hidden", 1, row.name, "exchange_rate");
|
||||
frm.set_df_property("taxes", "hidden", 1, frm.docname, "exchange_rate", cdn);
|
||||
} else if (!row.exchange_rate || row.exchange_rate == 1) {
|
||||
frm.set_df_property("taxes", "hidden", 0, row.name, "exchange_rate");
|
||||
frm.set_df_property("taxes", "hidden", 0, frm.docname, "exchange_rate", cdn);
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_exchange_rate",
|
||||
args: {
|
||||
|
||||
@@ -18,25 +18,27 @@
|
||||
<Nazione>{{ address.country_code }}</Nazione>
|
||||
{%- endmacro %}
|
||||
|
||||
{%- macro render_discount_or_margin(item) -%}
|
||||
{%- if (item.discount_percentage and item.discount_percentage > 0.0) or item.margin_type %}
|
||||
{%- macro render_discount_or_margin(item, tax_divisor) -%}
|
||||
{%- if item.discount_percentage and item.discount_percentage > 0.0 %}
|
||||
<ScontoMaggiorazione>
|
||||
{%- if item.discount_percentage > 0.0 %}
|
||||
<Tipo>SC</Tipo>
|
||||
<Percentuale>{{ format_float(item.discount_percentage) }}</Percentuale>
|
||||
{%- endif %}
|
||||
{%- if item.margin_rate_or_amount > 0.0 -%}
|
||||
<Tipo>MG</Tipo>
|
||||
{%- if item.margin_type == "Percentage" -%}
|
||||
<Percentuale>{{ format_float(item.margin_rate_or_amount) }}</Percentuale>
|
||||
{%- elif item.margin_type == "Amount" -%}
|
||||
<Importo>{{ format_float(item.margin_rate_or_amount) }}</Importo>
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
</ScontoMaggiorazione>
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
{%- if item.margin_rate_or_amount and item.margin_rate_or_amount > 0.0 %}
|
||||
<ScontoMaggiorazione>
|
||||
<Tipo>MG</Tipo>
|
||||
{%- if item.margin_type == "Percentage" -%}
|
||||
<Percentuale>{{ format_float(item.margin_rate_or_amount) }}</Percentuale>
|
||||
{%- elif item.margin_type == "Amount" -%}
|
||||
<Importo>{{ format_float(item.margin_rate_or_amount / tax_divisor) }}</Importo>
|
||||
{%- endif -%}
|
||||
</ScontoMaggiorazione>
|
||||
{%- endif %}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- set has_inclusive_tax = doc.taxes | selectattr("included_in_print_rate") | list | length > 0 -%}
|
||||
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<p:FatturaElettronica xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
|
||||
xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2"
|
||||
@@ -179,6 +181,7 @@
|
||||
</DatiGenerali>
|
||||
<DatiBeniServizi>
|
||||
{%- for item in doc.e_invoice_items %}
|
||||
{%- set tax_divisor = (1 + item.tax_rate / 100) if has_inclusive_tax and item.tax_rate else 1 %}
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>{{ item.idx }}</NumeroLinea>
|
||||
<CodiceArticolo>
|
||||
@@ -188,8 +191,9 @@
|
||||
<Descrizione>{{ html2text(item.description or '') or item.item_name }}</Descrizione>
|
||||
<Quantita>{{ format_float(item.qty) }}</Quantita>
|
||||
<UnitaMisura>{{ item.stock_uom }}</UnitaMisura>
|
||||
<PrezzoUnitario>{{ format_float(item.net_rate or item.price_list_rate or item.rate, item_meta.get_field("rate").precision) }}</PrezzoUnitario>
|
||||
{{ render_discount_or_margin(item) }}
|
||||
{%- set item_unit_net_price = (item.price_list_rate / tax_divisor) or (item.net_rate) or (item.rate / tax_divisor) %}
|
||||
<PrezzoUnitario>{{ format_float(item_unit_net_price, item_meta.get_field("rate").precision) }}</PrezzoUnitario>
|
||||
{{ render_discount_or_margin(item, tax_divisor) }}
|
||||
<PrezzoTotale>{{ format_float(item.net_amount, item_meta.get_field("amount").precision) }}</PrezzoTotale>
|
||||
<AliquotaIVA>{{ format_float(item.tax_rate, item_meta.get_field("tax_rate").precision) }}</AliquotaIVA>
|
||||
{%- if item.tax_exemption_reason %}
|
||||
|
||||
@@ -26,7 +26,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
let color;
|
||||
if (!doc.qty && frm.doc.has_unit_price_items) {
|
||||
color = "yellow";
|
||||
} else if (doc.stock_qty <= doc.delivered_qty) {
|
||||
} else if (doc.stock_qty <= doc.actual_qty) {
|
||||
color = "green";
|
||||
} else {
|
||||
color = "orange";
|
||||
|
||||
@@ -1003,20 +1003,19 @@ def close_or_unclose_sales_orders(names, status):
|
||||
|
||||
def get_requested_item_qty(sales_order):
|
||||
result = {}
|
||||
for d in frappe.db.get_all(
|
||||
"Material Request Item",
|
||||
filters={"docstatus": 1, "sales_order": sales_order},
|
||||
fields=[
|
||||
"sales_order_item",
|
||||
"packed_item",
|
||||
{"SUM": "qty", "as": "qty"},
|
||||
{"SUM": "received_qty", "as": "received_qty"},
|
||||
],
|
||||
group_by="sales_order_item, packed_item",
|
||||
):
|
||||
result[d.sales_order_item or d.packed_item] = frappe._dict(
|
||||
{"qty": d.qty, "received_qty": d.received_qty}
|
||||
)
|
||||
|
||||
so = frappe.get_doc("Sales Order", sales_order)
|
||||
|
||||
for item in so.items:
|
||||
if is_product_bundle(item.item_code):
|
||||
for packed_item in so.get("packed_items"):
|
||||
if (
|
||||
packed_item.parent_item == item.item_code
|
||||
and packed_item.parent_detail_docname == item.name
|
||||
):
|
||||
result[packed_item.name] = frappe._dict({"qty": packed_item.requested_qty})
|
||||
else:
|
||||
result[item.name] = frappe._dict({"qty": item.requested_qty})
|
||||
|
||||
return result
|
||||
|
||||
@@ -1035,8 +1034,7 @@ def make_material_request(source_name, target_doc=None):
|
||||
flt(so_item.qty)
|
||||
- flt(requested_item_qty.get(so_item.name, {}).get("qty"))
|
||||
- max(
|
||||
flt(so_item.get("delivered_qty"))
|
||||
- flt(requested_item_qty.get(so_item.name, {}).get("received_qty")),
|
||||
flt(so_item.get("delivered_qty")),
|
||||
0,
|
||||
)
|
||||
)
|
||||
@@ -1051,16 +1049,12 @@ def make_material_request(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return flt(
|
||||
(
|
||||
flt(so_item.qty)
|
||||
- flt(requested_item_qty.get(so_item.name, {}).get("qty"))
|
||||
- max(
|
||||
flt(delivered_qty) * flt(bundle_item_qty)
|
||||
- flt(requested_item_qty.get(so_item.name, {}).get("received_qty")),
|
||||
0,
|
||||
)
|
||||
flt(so_item.qty)
|
||||
- flt(requested_item_qty.get(so_item.name, {}).get("qty"))
|
||||
- max(
|
||||
flt(delivered_qty) * flt(bundle_item_qty),
|
||||
0,
|
||||
)
|
||||
* bundle_item_qty
|
||||
)
|
||||
|
||||
def update_item(source, target, source_parent):
|
||||
@@ -1122,8 +1116,10 @@ def make_material_request(source_name, target_doc=None):
|
||||
target_doc,
|
||||
postprocess,
|
||||
)
|
||||
|
||||
return doc
|
||||
if doc and doc.items:
|
||||
return doc
|
||||
else:
|
||||
frappe.throw(_("Material Request already created for the ordered quantity"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -57,6 +57,71 @@ class TestSalesOrder(AccountsTestMixin, IntegrationTestCase):
|
||||
frappe.db.rollback()
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Stock Settings",
|
||||
{
|
||||
"auto_insert_price_list_rate_if_missing": 1,
|
||||
"update_existing_price_list_rate": 1,
|
||||
"update_price_list_based_on": "Rate",
|
||||
},
|
||||
)
|
||||
def test_sales_order_expired_item_price(self):
|
||||
price_list = "_Test Price List"
|
||||
|
||||
item_1 = make_item("_Test Expired Item 1", {"is_stock_item": 1})
|
||||
|
||||
frappe.db.delete("Item Price", {"item_code": item_1.item_code})
|
||||
|
||||
item_price = frappe.new_doc("Item Price")
|
||||
item_price.item_code = item_1.item_code
|
||||
item_price.price_list = price_list
|
||||
item_price.price_list_rate = 100
|
||||
item_price.valid_from = add_days(today(), -10)
|
||||
item_price.valid_upto = add_days(today(), -5)
|
||||
item_price.save()
|
||||
|
||||
so = make_sales_order(
|
||||
item_code=item_1.item_code, qty=1, rate=1000, selling_price_list=price_list, do_not_save=True
|
||||
)
|
||||
so.save()
|
||||
so.reload()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Item Price", item_price.name, "price_list_rate"), 100)
|
||||
self.assertEqual(
|
||||
frappe.db.count("Item Price", {"item_code": item_1.item_code, "price_list": price_list}),
|
||||
2,
|
||||
)
|
||||
all_item_prices = frappe.get_all(
|
||||
"Item Price", filters={"item_code": item_1.item_code}, order_by="valid_from desc"
|
||||
)
|
||||
self.assertEqual(frappe.db.get_value("Item Price", all_item_prices[0].name, "price_list_rate"), 1000)
|
||||
|
||||
def test_sales_order_with_product_bundle_for_partial_material_request(self):
|
||||
product_bundle = make_product_bundle(
|
||||
"_Test Product Bundle Item", ["_Test Item", "_Test Item Home Desktop 100"]
|
||||
)
|
||||
so = make_sales_order(item_code=product_bundle.name, qty=2)
|
||||
mr = make_material_request(so.name)
|
||||
mr.items[0].qty = 4
|
||||
mr.items[1].qty = 2
|
||||
mr.items[0].schedule_date = today()
|
||||
mr.items[1].schedule_date = today()
|
||||
mr.save()
|
||||
mr.submit()
|
||||
mr.reload()
|
||||
self.assertEqual(mr.items[0].qty, 4)
|
||||
mr = make_material_request(so.name)
|
||||
self.assertEqual(mr.items[0].qty, 6)
|
||||
|
||||
def test_sales_order_with_full_material_request(self):
|
||||
so = make_sales_order(item_code="_Test Item", qty=5, do_not_submit=True)
|
||||
so.submit()
|
||||
mr = make_material_request(so.name)
|
||||
mr.save()
|
||||
mr.submit()
|
||||
mr.reload()
|
||||
self.assertRaises(frappe.ValidationError, make_material_request, so.name)
|
||||
|
||||
def test_sales_order_skip_delivery_note(self):
|
||||
so = make_sales_order(do_not_submit=True)
|
||||
so.order_type = "Maintenance"
|
||||
|
||||
@@ -323,13 +323,13 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-27 00:47:46.003305",
|
||||
"modified": "2026-03-16 13:28:18.988883",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling Settings",
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.127020",
|
||||
"letter_head": null,
|
||||
"modified": "2026-03-13 17:36:37.619715",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Trends",
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.096303",
|
||||
"letter_head": null,
|
||||
"modified": "2026-03-13 17:36:21.440118",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Trends",
|
||||
|
||||
@@ -91,13 +91,13 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-12 09:45:59.819161",
|
||||
"modified": "2026-03-16 13:28:20.155574",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Global Defaults",
|
||||
|
||||
@@ -251,6 +251,7 @@ class DeprecatedBatchNoValuation:
|
||||
.select(
|
||||
sle.batch_no,
|
||||
Sum(sle.actual_qty).as_("batch_qty"),
|
||||
Sum(sle.stock_value_difference).as_("batch_value"),
|
||||
)
|
||||
.where(
|
||||
(sle.item_code == self.sle.item_code)
|
||||
@@ -267,9 +268,24 @@ class DeprecatedBatchNoValuation:
|
||||
if self.sle.name:
|
||||
query = query.where(sle.name != self.sle.name)
|
||||
|
||||
# Moving Average items with no Use Batch wise Valuation but want to use batch wise valuation
|
||||
moving_avg_item_non_batch_value = False
|
||||
if valuation_method := self.get_valuation_method(self.sle.item_code):
|
||||
if valuation_method == "Moving Average" and not frappe.db.get_single_value(
|
||||
"Stock Settings", "do_not_use_batchwise_valuation"
|
||||
):
|
||||
query = query.where(batch.use_batchwise_valuation == 0)
|
||||
moving_avg_item_non_batch_value = True
|
||||
|
||||
batch_data = query.run(as_dict=True)
|
||||
for d in batch_data:
|
||||
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
||||
if moving_avg_item_non_batch_value:
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty)
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value)
|
||||
|
||||
if moving_avg_item_non_batch_value:
|
||||
return
|
||||
|
||||
for d in batch_data:
|
||||
if self.available_qty.get(d.batch_no):
|
||||
@@ -378,9 +394,24 @@ class DeprecatedBatchNoValuation:
|
||||
|
||||
query = query.where(bundle.voucher_type != "Pick List")
|
||||
|
||||
# Moving Average items with no Use Batch wise Valuation but want to use batch wise valuation
|
||||
moving_avg_item_non_batch_value = False
|
||||
if valuation_method := self.get_valuation_method(self.sle.item_code):
|
||||
if valuation_method == "Moving Average" and not frappe.db.get_single_value(
|
||||
"Stock Settings", "do_not_use_batchwise_valuation"
|
||||
):
|
||||
query = query.where(batch.use_batchwise_valuation == 0)
|
||||
moving_avg_item_non_batch_value = True
|
||||
|
||||
batch_data = query.run(as_dict=True)
|
||||
for d in batch_data:
|
||||
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
||||
if moving_avg_item_non_batch_value:
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty)
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value)
|
||||
|
||||
if moving_avg_item_non_batch_value:
|
||||
return
|
||||
|
||||
if not self.last_sle:
|
||||
return
|
||||
@@ -388,3 +419,8 @@ class DeprecatedBatchNoValuation:
|
||||
for batch_no in self.available_qty:
|
||||
self.non_batchwise_balance_value[batch_no] = flt(self.last_sle.stock_value)
|
||||
self.non_batchwise_balance_qty[batch_no] = flt(self.last_sle.qty_after_transaction)
|
||||
|
||||
def get_valuation_method(self, item_code):
|
||||
from erpnext.stock.utils import get_valuation_method
|
||||
|
||||
return get_valuation_method(item_code, self.sle.company)
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_company_address
|
||||
from frappe.contacts.doctype.contact.contact import get_default_contact
|
||||
from frappe.desk.notifications import clear_doctype_notifications
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.utils import get_fetch_values
|
||||
@@ -395,6 +396,9 @@ class DeliveryNote(SellingController):
|
||||
)
|
||||
|
||||
def validate_sales_invoice_references(self):
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
self._validate_dependent_item_fields(
|
||||
"against_sales_invoice", "si_detail", _("References to Sales Invoices are Incomplete")
|
||||
)
|
||||
@@ -978,6 +982,11 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
|
||||
def make_delivery_trip(source_name, target_doc=None, kwargs=None):
|
||||
if not target_doc:
|
||||
target_doc = frappe.new_doc("Delivery Trip")
|
||||
|
||||
def update_address(source_doc, target_doc, source_parent):
|
||||
target_doc.address = source_doc.shipping_address_name or source_doc.customer_address
|
||||
target_doc.customer_address = source_doc.shipping_address or source_doc.address_display
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Delivery Note",
|
||||
source_name,
|
||||
@@ -987,11 +996,10 @@ def make_delivery_trip(source_name, target_doc=None, kwargs=None):
|
||||
"on_parent": target_doc,
|
||||
"field_map": {
|
||||
"name": "delivery_note",
|
||||
"shipping_address_name": "address",
|
||||
"shipping_address": "customer_address",
|
||||
"contact_person": "contact",
|
||||
"contact_display": "customer_contact",
|
||||
},
|
||||
"postprocess": update_address,
|
||||
},
|
||||
},
|
||||
ignore_child_tables=True,
|
||||
@@ -1103,18 +1111,24 @@ def make_shipment(source_name, target_doc=None):
|
||||
# As we are using session user details in the pickup_contact then pickup_contact_person will be session user
|
||||
target.pickup_contact_person = frappe.session.user
|
||||
|
||||
if source.contact_person:
|
||||
contact_person = source.contact_person or get_default_contact("Customer", source.customer)
|
||||
if contact_person:
|
||||
contact = frappe.db.get_value(
|
||||
"Contact", source.contact_person, ["email_id", "phone", "mobile_no"], as_dict=1
|
||||
"Contact", contact_person, ["email_id", "phone", "mobile_no"], as_dict=1
|
||||
)
|
||||
delivery_contact_display = f"{source.contact_display}"
|
||||
if contact:
|
||||
|
||||
delivery_contact_display = source.contact_display or contact_person or ""
|
||||
if contact and not source.contact_display:
|
||||
if contact.email_id:
|
||||
delivery_contact_display += "<br>" + contact.email_id
|
||||
if contact.phone:
|
||||
delivery_contact_display += "<br>" + contact.phone
|
||||
if contact.mobile_no and not contact.phone:
|
||||
delivery_contact_display += "<br>" + contact.mobile_no
|
||||
|
||||
target.delivery_contact_name = contact_person
|
||||
if contact and contact.email_id and not target.delivery_contact_email:
|
||||
target.delivery_contact_email = contact.email_id
|
||||
target.delivery_contact = delivery_contact_display
|
||||
|
||||
if source.shipping_address_name:
|
||||
|
||||
@@ -50,10 +50,10 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:18:39.931428",
|
||||
"modified": "2026-03-16 13:28:20.371015",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Settings",
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"bold": 1,
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
@@ -357,6 +358,7 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "is_stock_item",
|
||||
"documentation_url": "https://docs.frappe.io/erpnext/change-valuation-method",
|
||||
"fieldname": "valuation_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Valuation Method",
|
||||
@@ -987,7 +989,7 @@
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2026-03-05 16:29:31.653447",
|
||||
"modified": "2026-03-17 20:39:05.218344",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
"label": "Allow Variant UOM to be different from Template UOM"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:18:23.005859",
|
||||
"modified": "2026-03-16 13:28:20.597912",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Variant Settings",
|
||||
|
||||
@@ -116,12 +116,12 @@ frappe.ui.form.on("Material Request", {
|
||||
refresh: function (frm) {
|
||||
frm.events.make_custom_buttons(frm);
|
||||
frm.toggle_reqd("customer", frm.doc.material_request_type == "Customer Provided");
|
||||
prevent_past_schedule_dates(frm);
|
||||
erpnext.buying.prevent_past_schedule_dates(frm);
|
||||
frm.trigger("set_warehouse_label");
|
||||
},
|
||||
|
||||
transaction_date(frm) {
|
||||
prevent_past_schedule_dates(frm);
|
||||
erpnext.buying.prevent_past_schedule_dates(frm);
|
||||
frm.set_value("schedule_date", "");
|
||||
},
|
||||
|
||||
@@ -410,33 +410,11 @@ frappe.ui.form.on("Material Request", {
|
||||
},
|
||||
|
||||
make_purchase_order: function (frm) {
|
||||
frappe.prompt(
|
||||
{
|
||||
label: __("For Default Supplier (Optional)"),
|
||||
fieldname: "default_supplier",
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
description: __(
|
||||
"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only."
|
||||
),
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.stock.doctype.material_request.material_request.get_default_supplier_query",
|
||||
filters: { doc: frm.doc.name },
|
||||
};
|
||||
},
|
||||
},
|
||||
(values) => {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order",
|
||||
frm: frm,
|
||||
args: { default_supplier: values.default_supplier },
|
||||
run_link_triggers: true,
|
||||
});
|
||||
},
|
||||
__("Enter Supplier"),
|
||||
__("Create")
|
||||
);
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order",
|
||||
frm: frm,
|
||||
run_link_triggers: true,
|
||||
});
|
||||
},
|
||||
|
||||
make_request_for_quotation: function (frm) {
|
||||
@@ -681,11 +659,3 @@ function set_schedule_date(frm) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function prevent_past_schedule_dates(frm) {
|
||||
if (frm.doc.transaction_date) {
|
||||
frm.fields_dict["schedule_date"].datepicker.update({
|
||||
minDate: new Date(frm.doc.transaction_date),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_se
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty
|
||||
from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
|
||||
get_subcontracting_boms_for_finished_goods,
|
||||
@@ -96,7 +95,16 @@ class MaterialRequest(BuyingController):
|
||||
"join_field": "sales_order_item",
|
||||
"target_ref_field": "stock_qty",
|
||||
"source_field": "stock_qty",
|
||||
}
|
||||
},
|
||||
{
|
||||
"source_dt": "Material Request Item",
|
||||
"target_dt": "Packed Item",
|
||||
"target_field": "requested_qty",
|
||||
"target_parent_dt": "Sales Order",
|
||||
"join_field": "packed_item",
|
||||
"target_ref_field": "qty",
|
||||
"source_field": "qty",
|
||||
},
|
||||
]
|
||||
|
||||
def check_if_already_pulled(self):
|
||||
@@ -501,17 +509,6 @@ def make_purchase_order(source_name, target_doc=None, args=None):
|
||||
|
||||
def postprocess(source, target_doc):
|
||||
target_doc.is_subcontracted = is_subcontracted
|
||||
if frappe.flags.args and frappe.flags.args.default_supplier:
|
||||
# items only for given default supplier
|
||||
supplier_items = []
|
||||
for d in target_doc.items:
|
||||
if is_subcontracted and not d.item_code:
|
||||
continue
|
||||
default_supplier = get_item_defaults(d.item_code, target_doc.company).get("default_supplier")
|
||||
if frappe.flags.args.default_supplier == default_supplier:
|
||||
supplier_items.append(d)
|
||||
target_doc.items = supplier_items
|
||||
|
||||
set_missing_values(source, target_doc)
|
||||
|
||||
def select_item(d):
|
||||
@@ -689,37 +686,6 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa
|
||||
return material_requests
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doc = frappe.get_doc("Material Request", filters.get("doc"))
|
||||
item_list = []
|
||||
for d in doc.items:
|
||||
item_list.append(d.item_code)
|
||||
|
||||
supplier = frappe.qb.DocType("Supplier")
|
||||
item_default = frappe.qb.DocType("Item Default")
|
||||
query = (
|
||||
frappe.qb.from_(supplier)
|
||||
.left_join(item_default)
|
||||
.on(supplier.name == item_default.default_supplier)
|
||||
.select(item_default.default_supplier)
|
||||
.where(
|
||||
(item_default.parent.isin(item_list))
|
||||
& (item_default.default_supplier.notnull())
|
||||
& (supplier[searchfield].like(f"%{txt}%"))
|
||||
)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
meta = frappe.get_meta("Supplier")
|
||||
if meta.show_title_field_in_link and meta.title_field:
|
||||
query = query.select(supplier[meta.title_field])
|
||||
|
||||
return query.run(as_dict=False)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_supplier_quotation(source_name, target_doc=None):
|
||||
def postprocess(source, target_doc):
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"projected_qty",
|
||||
"ordered_qty",
|
||||
"packed_qty",
|
||||
"requested_qty",
|
||||
"column_break_16",
|
||||
"incoming_rate",
|
||||
"picked_qty",
|
||||
@@ -298,13 +299,22 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Supplier delivers to Customer",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "requested_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Requested Qty",
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-09 19:12:45.850219",
|
||||
"modified": "2026-03-16 18:10:47.511381",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Packed Item",
|
||||
|
||||
@@ -45,6 +45,7 @@ class PackedItem(Document):
|
||||
projected_qty: DF.Float
|
||||
qty: DF.Float
|
||||
rate: DF.Currency
|
||||
requested_qty: DF.Float
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.Text | None
|
||||
target_warehouse: DF.Link | None
|
||||
@@ -354,11 +355,19 @@ def update_product_bundle_rate(parent_items_price, pi_row, item_row):
|
||||
|
||||
def set_product_bundle_rate_amount(doc, parent_items_price):
|
||||
"Set cumulative rate and amount in bundle item."
|
||||
rate_updated = False
|
||||
for item in doc.get("items"):
|
||||
bundle_rate = parent_items_price.get((item.item_code, item.name))
|
||||
if bundle_rate and bundle_rate != item.rate:
|
||||
item.rate = bundle_rate
|
||||
item.amount = flt(bundle_rate * item.qty)
|
||||
item.margin_rate_or_amount = 0
|
||||
item.discount_percentage = 0
|
||||
item.discount_amount = 0
|
||||
rate_updated = True
|
||||
if rate_updated:
|
||||
doc.calculate_taxes_and_totals()
|
||||
doc.set_total_in_words()
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
|
||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Purchase Receipt", {
|
||||
|
||||
frm.set_query("wip_composite_asset", "items", function () {
|
||||
return {
|
||||
filters: { is_composite_asset: 1, docstatus: 0 },
|
||||
filters: { asset_type: "Composite Asset", docstatus: 0 },
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1714,17 +1714,34 @@ def upload_csv_file(item_code, file_path):
|
||||
|
||||
|
||||
def get_serial_batch_from_csv(item_code, file_path):
|
||||
if "private" in file_path:
|
||||
file_path = frappe.get_site_path() + file_path
|
||||
else:
|
||||
file_path = frappe.get_site_path() + "/public" + file_path
|
||||
from frappe.utils.csvutils import read_csv_content
|
||||
|
||||
serial_nos = []
|
||||
batch_nos = []
|
||||
|
||||
with open(file_path) as f:
|
||||
reader = csv.reader(f)
|
||||
serial_nos, batch_nos = parse_csv_file_to_get_serial_batch(reader)
|
||||
if not file_path:
|
||||
return serial_nos, batch_nos
|
||||
|
||||
try:
|
||||
file = frappe.get_doc("File", {"file_url": file_path})
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.msgprint(
|
||||
_("File '{0}' not found").format(frappe.bold(file_path)),
|
||||
alert=True,
|
||||
indicator="red",
|
||||
raise_exception=FileNotFoundError,
|
||||
)
|
||||
|
||||
if file.file_type != "CSV":
|
||||
frappe.msgprint(
|
||||
_("{0} is not a CSV file.").format(frappe.bold(file.file_name)),
|
||||
alert=True,
|
||||
indicator="red",
|
||||
raise_exception=frappe.ValidationError,
|
||||
)
|
||||
|
||||
csv_data = read_csv_content(file.get_content())
|
||||
serial_nos, batch_nos = parse_csv_file_to_get_serial_batch(csv_data)
|
||||
|
||||
if serial_nos:
|
||||
make_serial_nos(item_code, serial_nos)
|
||||
@@ -2808,7 +2825,7 @@ def get_auto_batch_nos(kwargs):
|
||||
)
|
||||
|
||||
if kwargs.based_on == "Expiry":
|
||||
available_batches = sorted(available_batches, key=lambda x: (x.expiry_date or getdate("9999-12-31")))
|
||||
available_batches = sorted(available_batches, key=lambda x: x.expiry_date or getdate("9999-12-31"))
|
||||
|
||||
if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_datetime"):
|
||||
filter_zero_near_batches(available_batches, kwargs)
|
||||
|
||||
@@ -359,6 +359,136 @@ class TestSerialandBatchBundle(IntegrationTestCase):
|
||||
self.assertFalse(json.loads(sle.stock_queue or "[]"))
|
||||
self.assertEqual(flt(sle.stock_value), 0.0)
|
||||
|
||||
def test_old_moving_avg_item_with_without_batchwise_valuation(self):
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||
frappe.flags.use_serial_and_batch_fields = True
|
||||
batch_item_code = "Old Batch Item Valuation 2"
|
||||
make_item(
|
||||
batch_item_code,
|
||||
{
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "TEST-OLD2-BAT-VAL-.#####",
|
||||
"create_new_batch": 1,
|
||||
"is_stock_item": 1,
|
||||
"valuation_method": "Moving Average",
|
||||
},
|
||||
)
|
||||
|
||||
non_batchwise_val_batches = [
|
||||
"TEST-OLD2-BAT-VAL-00001",
|
||||
"TEST-OLD2-BAT-VAL-00002",
|
||||
"TEST-OLD2-BAT-VAL-00003",
|
||||
"TEST-OLD2-BAT-VAL-00004",
|
||||
]
|
||||
|
||||
for batch_id in non_batchwise_val_batches:
|
||||
if not frappe.db.exists("Batch", batch_id):
|
||||
batch_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_id,
|
||||
"item": batch_item_code,
|
||||
"use_batchwise_valuation": 0,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
self.assertTrue(batch_doc.use_batchwise_valuation)
|
||||
batch_doc.db_set(
|
||||
{
|
||||
"use_batchwise_valuation": 0,
|
||||
"batch_qty": 20,
|
||||
}
|
||||
)
|
||||
|
||||
qty_after_transaction = 0
|
||||
balance_value = 0
|
||||
i = 0
|
||||
for batch_id in non_batchwise_val_batches:
|
||||
i += 1
|
||||
qty = 20
|
||||
valuation = 100 * i
|
||||
qty_after_transaction += qty
|
||||
balance_value += qty * valuation
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Stock Ledger Entry",
|
||||
"posting_date": today(),
|
||||
"posting_time": nowtime(),
|
||||
"batch_no": batch_id,
|
||||
"incoming_rate": valuation,
|
||||
"qty_after_transaction": qty_after_transaction,
|
||||
"stock_value_difference": valuation * qty,
|
||||
"stock_value": balance_value,
|
||||
"balance_value": balance_value,
|
||||
"valuation_rate": balance_value / qty_after_transaction,
|
||||
"actual_qty": qty,
|
||||
"item_code": batch_item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
}
|
||||
)
|
||||
|
||||
doc.set_posting_datetime()
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.submit()
|
||||
doc.reload()
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=batch_item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=30,
|
||||
rate=355,
|
||||
use_serial_batch_fields=True,
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=batch_item_code,
|
||||
source="_Test Warehouse - _TC",
|
||||
qty=70,
|
||||
use_serial_batch_fields=True,
|
||||
)
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name},
|
||||
["qty_after_transaction", "stock_value"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(flt(sle.stock_value), 14000.0)
|
||||
self.assertEqual(flt(sle.qty_after_transaction), 40.0)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=batch_item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=10,
|
||||
rate=200,
|
||||
use_serial_batch_fields=True,
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=batch_item_code,
|
||||
source="_Test Warehouse - _TC",
|
||||
qty=50,
|
||||
use_serial_batch_fields=True,
|
||||
)
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name},
|
||||
["qty_after_transaction", "stock_value"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(flt(sle.stock_value), 0.0)
|
||||
self.assertEqual(flt(sle.qty_after_transaction), 0.0)
|
||||
|
||||
def test_old_serial_no_valuation(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
|
||||
@@ -524,11 +524,11 @@ class StockReconciliation(StockController):
|
||||
return True
|
||||
|
||||
rate_precision = item.precision("valuation_rate")
|
||||
item_dict["rate"] = flt(item_dict.get("rate"), rate_precision)
|
||||
item.valuation_rate = flt(item.valuation_rate, rate_precision) if item.valuation_rate else None
|
||||
rate = flt(item_dict.get("rate"), rate_precision)
|
||||
valuation_rate = flt(item.valuation_rate, rate_precision) if item.valuation_rate else None
|
||||
if (
|
||||
(item.qty is None or item.qty == item_dict.get("qty"))
|
||||
and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))
|
||||
and (valuation_rate is None or valuation_rate == rate)
|
||||
and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")))
|
||||
):
|
||||
return False
|
||||
@@ -1008,9 +1008,9 @@ class StockReconciliation(StockController):
|
||||
|
||||
def set_total_qty_and_amount(self):
|
||||
for d in self.get("items"):
|
||||
d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate"))
|
||||
d.current_amount = flt(d.current_qty, d.precision("current_qty")) * flt(
|
||||
d.current_valuation_rate, d.precision("current_valuation_rate")
|
||||
d.amount = flt(flt(d.qty) * flt(d.valuation_rate), d.precision("amount"))
|
||||
d.current_amount = flt(
|
||||
flt(d.current_qty) * flt(d.current_valuation_rate), d.precision("current_amount")
|
||||
)
|
||||
|
||||
d.quantity_difference = flt(d.qty) - flt(d.current_qty)
|
||||
|
||||
@@ -101,11 +101,11 @@
|
||||
"label": "Enable Separate Reposting for GL"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-25 14:11:33.461173",
|
||||
"modified": "2026-03-16 13:28:20.978007",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reposting Settings",
|
||||
|
||||
@@ -558,13 +558,13 @@
|
||||
"label": "Enable Serial / Batch No for Item"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-25 10:56:34.105949",
|
||||
"modified": "2026-03-16 13:28:19.254641",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
|
||||
@@ -114,6 +114,22 @@ class StockSettings(Document):
|
||||
self.validate_auto_insert_price_list_rate_if_missing()
|
||||
self.change_precision_for_for_sales()
|
||||
self.change_precision_for_purchase()
|
||||
self.validate_do_not_use_batchwise_valuation()
|
||||
|
||||
def validate_do_not_use_batchwise_valuation(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save:
|
||||
return
|
||||
|
||||
if not frappe.get_all("Serial and Batch Bundle", filters={"docstatus": 1}, limit=1, pluck="name"):
|
||||
return
|
||||
|
||||
if doc_before_save.do_not_use_batchwise_valuation and not self.do_not_use_batchwise_valuation:
|
||||
frappe.throw(
|
||||
_("Cannot disable {0} as it may lead to incorrect stock valuation.").format(
|
||||
frappe.bold(_("Do Not Use Batchwise Valuation"))
|
||||
)
|
||||
)
|
||||
|
||||
def validate_serial_and_batch_no_settings(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
|
||||
@@ -13,7 +13,7 @@ from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate, parse_json
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, get_link_to_form, getdate, parse_json
|
||||
|
||||
import erpnext
|
||||
from erpnext import get_company_currency
|
||||
@@ -1055,16 +1055,30 @@ def insert_item_price(ctx: ItemDetailsCtx):
|
||||
):
|
||||
return
|
||||
|
||||
item_price = frappe.db.get_value(
|
||||
transaction_date = (
|
||||
getdate(ctx.get("posting_date") or ctx.get("transaction_date") or ctx.get("posting_datetime"))
|
||||
or getdate()
|
||||
)
|
||||
|
||||
item_prices = frappe.get_all(
|
||||
"Item Price",
|
||||
{
|
||||
filters={
|
||||
"item_code": ctx.item_code,
|
||||
"price_list": ctx.price_list,
|
||||
"currency": ctx.currency,
|
||||
"uom": ctx.stock_uom,
|
||||
},
|
||||
["name", "price_list_rate"],
|
||||
as_dict=1,
|
||||
fields=["name", "price_list_rate", "valid_from", "valid_upto"],
|
||||
order_by="valid_from desc, creation desc",
|
||||
)
|
||||
item_price = next(
|
||||
(
|
||||
row
|
||||
for row in item_prices
|
||||
if (not row.valid_from or getdate(row.valid_from) <= transaction_date)
|
||||
and (not row.valid_upto or getdate(row.valid_upto) >= transaction_date)
|
||||
),
|
||||
item_prices[0] if item_prices else None,
|
||||
)
|
||||
|
||||
update_based_on_price_list_rate = stock_settings.update_price_list_based_on == "Price List Rate"
|
||||
@@ -1079,11 +1093,33 @@ def insert_item_price(ctx: ItemDetailsCtx):
|
||||
if not price_list_rate or item_price.price_list_rate == price_list_rate:
|
||||
return
|
||||
|
||||
frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
|
||||
frappe.msgprint(
|
||||
_("Item Price updated for {0} in Price List {1}").format(ctx.item_code, ctx.price_list),
|
||||
alert=True,
|
||||
)
|
||||
is_price_valid_for_transaction = (
|
||||
not item_price.valid_from or getdate(item_price.valid_from) <= transaction_date
|
||||
) and (not item_price.valid_upto or getdate(item_price.valid_upto) >= transaction_date)
|
||||
if is_price_valid_for_transaction:
|
||||
frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
|
||||
frappe.msgprint(
|
||||
_("Item Price updated for {0} in Price List {1}").format(ctx.item_code, ctx.price_list),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
# if price is not valid for the transaction date, insert a new price list rate with updated price and future validity
|
||||
|
||||
item_price = frappe.new_doc(
|
||||
"Item Price",
|
||||
item_code=ctx.item_code,
|
||||
price_list_rate=price_list_rate,
|
||||
currency=ctx.currency,
|
||||
uom=ctx.stock_uom,
|
||||
price_list=ctx.price_list,
|
||||
)
|
||||
item_price.insert()
|
||||
frappe.msgprint(
|
||||
_("Item Price Added for {0} in Price List {1}").format(
|
||||
get_link_to_form("Item", ctx.item_code), ctx.price_list
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
rate_to_consider = (
|
||||
(flt(ctx.price_list_rate) or flt(ctx.rate)) if update_based_on_price_list_rate else flt(ctx.rate)
|
||||
@@ -1102,8 +1138,9 @@ def insert_item_price(ctx: ItemDetailsCtx):
|
||||
)
|
||||
item_price.insert()
|
||||
frappe.msgprint(
|
||||
_("Item Price added for {0} in Price List {1}").format(ctx.item_code, ctx.price_list),
|
||||
alert=True,
|
||||
_("Item Price added for {0} in Price List {1}").format(
|
||||
get_link_to_form("Item", ctx.item_code), ctx.price_list
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.114173",
|
||||
"letter_head": null,
|
||||
"modified": "2026-03-13 17:36:31.552712",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Trends",
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:49.983683",
|
||||
"letter_head": null,
|
||||
"modified": "2026-03-13 17:35:57.060786",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Trends",
|
||||
|
||||
@@ -925,7 +925,10 @@ class update_entries_after:
|
||||
if (
|
||||
sle.is_adjustment_entry
|
||||
and flt(sle.qty_after_transaction, self.flt_precision) == 0
|
||||
and flt(sle.stock_value, self.currency_precision) != 0
|
||||
and (
|
||||
flt(sle.stock_value, self.currency_precision) != 0
|
||||
or flt(sle.stock_value_difference, self.currency_precision) == 0
|
||||
)
|
||||
):
|
||||
sle.stock_value_difference = (
|
||||
get_stock_value_difference(
|
||||
|
||||
@@ -227,8 +227,14 @@ def set_status(name, status):
|
||||
|
||||
|
||||
def auto_close_tickets():
|
||||
"""Auto-close replied support tickets after 7 days"""
|
||||
auto_close_after_days = frappe.db.get_single_value("Support Settings", "close_issue_after_days") or 7
|
||||
"""
|
||||
Auto-close replied support tickets as defined on `close_issue_after_days` in Support Settings.
|
||||
Disables the feature if `close_issue_after_days` is set to 0.
|
||||
"""
|
||||
auto_close_after_days = frappe.db.get_single_value("Support Settings", "close_issue_after_days")
|
||||
|
||||
if not auto_close_after_days:
|
||||
return
|
||||
|
||||
table = frappe.qb.DocType("Issue")
|
||||
issues = (
|
||||
|
||||
@@ -37,9 +37,11 @@
|
||||
},
|
||||
{
|
||||
"default": "7",
|
||||
"description": "Set this value to 0 to disable the feature.",
|
||||
"fieldname": "close_issue_after_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Close Issue After Days"
|
||||
"label": "Close Issue After Days",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "portal_sb",
|
||||
@@ -154,10 +156,10 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:18:11.475222",
|
||||
"modified": "2026-03-16 16:33:45.859541",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Support",
|
||||
"name": "Support Settings",
|
||||
|
||||
@@ -81,4 +81,5 @@ docstring-code-format = true
|
||||
[project.urls]
|
||||
Homepage = "https://frappe.io/erpnext"
|
||||
Repository = "https://github.com/frappe/erpnext.git"
|
||||
Documentation = "https://docs.frappe.io/erpnext"
|
||||
"Bug Reports" = "https://github.com/frappe/erpnext/issues"
|
||||
|
||||
Reference in New Issue
Block a user