Merge pull request #46263 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-03-05 18:59:38 +05:30
committed by GitHub
114 changed files with 1676 additions and 414 deletions

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _, throw from frappe import _, throw
from frappe.utils import cint, cstr from frappe.utils import add_to_date, cint, cstr, pretty_date
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
import erpnext import erpnext
@@ -481,6 +481,7 @@ def get_account_autoname(account_number, account_name, company):
@frappe.whitelist() @frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False): def update_account_number(name, account_name, account_number=None, from_descendant=False):
_ensure_idle_system()
account = frappe.get_cached_doc("Account", name) account = frappe.get_cached_doc("Account", name)
if not account: if not account:
return return
@@ -542,6 +543,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist() @frappe.whitelist()
def merge_account(old, new): def merge_account(old, new):
_ensure_idle_system()
# Validate properties before merging # Validate properties before merging
new_account = frappe.get_cached_doc("Account", new) new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old) old_account = frappe.get_cached_doc("Account", old)
@@ -595,3 +597,27 @@ def sync_update_account_number_in_child(
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True): for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
update_account_number(d["name"], account_name, account_number, from_descendant=True) update_account_number(d["name"], account_name, account_number, from_descendant=True)
def _ensure_idle_system():
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
if frappe.flags.in_test:
return
try:
# We also lock inserts to GL entry table with for_update here.
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
except frappe.QueryTimeoutError:
# wait=False fails immediately if there's an active transaction.
last_gl_update = add_to_date(None, seconds=-1)
if last_gl_update > add_to_date(None, minutes=-5):
frappe.throw(
_(
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
).format(pretty_date(last_gl_update)),
title=_("System In Use"),
)

View File

@@ -258,6 +258,10 @@ frappe.ui.form.on("Payment Entry", {
frappe.flags.allocate_payment_amount = true; frappe.flags.allocate_payment_amount = true;
}, },
validate: async function (frm) {
await frm.events.set_exchange_gain_loss_deduction(frm);
},
validate_company: (frm) => { validate_company: (frm) => {
if (!frm.doc.company) { if (!frm.doc.company) {
frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") }); frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") });
@@ -1893,8 +1897,6 @@ function prompt_for_missing_account(frm, account) {
(values) => resolve(values?.[account]), (values) => resolve(values?.[account]),
__("Please Specify Account") __("Please Specify Account")
); );
dialog.on_hide = () => resolve("");
}); });
} }

View File

@@ -1843,7 +1843,7 @@ class PaymentEntry(AccountsController):
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
elif self.party_type in ("Supplier", "Employee"): elif self.party_type in ("Supplier", "Customer"):
if paid_amount > total_negative_outstanding: if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0: if total_negative_outstanding == 0:
frappe.msgprint( frappe.msgprint(
@@ -3337,13 +3337,14 @@ def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
"""Add loss on income discount in base currency.""" """Add loss on income discount in base currency."""
precision = doc.precision("total") precision = doc.precision("total")
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100) base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
positive_negative = -1 if pe.payment_type == "Pay" else 1
pe.append( pe.append(
"deductions", "deductions",
{ {
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": flt(base_loss_on_income, precision), "amount": flt(base_loss_on_income, precision) * positive_negative,
}, },
) )
@@ -3355,6 +3356,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
tax_discount_loss = {} tax_discount_loss = {}
base_total_tax_loss = 0 base_total_tax_loss = 0
precision = doc.precision("tax_amount_after_discount_amount", "taxes") precision = doc.precision("tax_amount_after_discount_amount", "taxes")
positive_negative = -1 if pe.payment_type == "Pay" else 1
# The same account head could be used more than once # The same account head could be used more than once
for tax in doc.get("taxes", []): for tax in doc.get("taxes", []):
@@ -3377,7 +3379,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
"account": account, "account": account,
"cost_center": pe.cost_center "cost_center": pe.cost_center
or frappe.get_cached_value("Company", pe.company, "cost_center"), or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": flt(loss, precision), "amount": flt(loss, precision) * positive_negative,
}, },
) )

View File

@@ -282,6 +282,48 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0) self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0) self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
def test_payment_entry_against_payment_terms_with_discount_on_pi(self):
pi = make_purchase_invoice(do_not_save=1)
create_payment_terms_template_with_discount()
pi.payment_terms_template = "Test Discount Template"
frappe.db.set_value("Company", pi.company, "default_discount_account", "Write Off - _TC")
pi.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 18,
},
)
pi.save()
pi.submit()
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
pe_with_tax_loss = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(pe_with_tax_loss.payment_type, "Pay")
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 295.0)
self.assertEqual(pe_with_tax_loss.paid_amount, 265.5)
self.assertEqual(pe_with_tax_loss.difference_amount, 0)
self.assertEqual(pe_with_tax_loss.deductions[0].amount, -25.0) # Loss on Income
self.assertEqual(pe_with_tax_loss.deductions[1].amount, -4.5) # Loss on Tax
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(pe.payment_type, "Pay")
self.assertEqual(pe.references[0].allocated_amount, 295.0)
self.assertEqual(pe.paid_amount, 265.5)
self.assertEqual(pe.deductions[0].amount, -29.5)
self.assertEqual(pe.difference_amount, 0)
def test_payment_entry_against_payment_terms_with_discount(self): def test_payment_entry_against_payment_terms_with_discount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200) si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template_with_discount() create_payment_terms_template_with_discount()

View File

@@ -768,29 +768,39 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
def get_existing_paid_amount(doctype, name): def get_existing_paid_amount(doctype, name):
PL = frappe.qb.DocType("Payment Ledger Entry") PLE = frappe.qb.DocType("Payment Ledger Entry")
PER = frappe.qb.DocType("Payment Entry Reference") PER = frappe.qb.DocType("Payment Entry Reference")
query = ( query = (
frappe.qb.from_(PL) frappe.qb.from_(PLE)
.left_join(PER) .left_join(PER)
.on( .on(
(PL.against_voucher_type == PER.reference_doctype) (PLE.against_voucher_type == PER.reference_doctype)
& (PL.against_voucher_no == PER.reference_name) & (PLE.against_voucher_no == PER.reference_name)
& (PL.voucher_type == PER.parenttype) & (PLE.voucher_type == PER.parenttype)
& (PL.voucher_no == PER.parent) & (PLE.voucher_no == PER.parent)
)
.select(
Abs(Sum(PLE.amount)).as_("total_amount"),
Abs(Sum(frappe.qb.terms.Case().when(PER.payment_request.isnotnull(), PLE.amount).else_(0))).as_(
"request_paid_amount"
),
)
.where(
(PLE.voucher_type.isin([doctype, "Journal Entry", "Payment Entry"]))
& (PLE.against_voucher_type == doctype)
& (PLE.against_voucher_no == name)
& (PLE.delinked == 0)
& (PLE.docstatus == 1)
& (PLE.amount < 0)
) )
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
.where(PL.against_voucher_type.eq(doctype))
.where(PL.against_voucher_no.eq(name))
.where(PL.amount < 0)
.where(PL.delinked == 0)
.where(PER.docstatus == 1)
.where(PER.payment_request.isnull())
) )
response = query.run()
return response[0][0] if response[0] else 0 result = query.run()
ledger_amount = flt(result[0][0]) if result else 0
request_paid_amount = flt(result[0][1]) if result else 0
return ledger_amount - request_paid_amount
def get_gateway_details(args): # nosemgrep def get_gateway_details(args): # nosemgrep

View File

@@ -581,6 +581,34 @@ class TestPaymentRequest(FrappeTestCase):
pi.load_from_db() pi.load_from_db()
self.assertEqual(pr_2.grand_total, pi.outstanding_amount) self.assertEqual(pr_2.grand_total, pi.outstanding_amount)
def test_consider_journal_entry_and_return_invoice(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
si = create_sales_invoice(currency="INR", qty=5, rate=500)
je = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 500, save=False)
je.accounts[1].party_type = "Customer"
je.accounts[1].party = si.customer
je.accounts[1].reference_type = "Sales Invoice"
je.accounts[1].reference_name = si.name
je.accounts[1].credit_in_account_currency = 500
je.submit()
pe = get_payment_entry("Sales Invoice", si.name)
pe.paid_amount = 500
pe.references[0].allocated_amount = 500
pe.save()
pe.submit()
cr_note = create_sales_invoice(qty=-1, rate=500, is_return=1, return_against=si.name, do_not_save=1)
cr_note.update_outstanding_for_self = 0
cr_note.save()
cr_note.submit()
si.load_from_db()
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
self.assertEqual(pr.grand_total, si.outstanding_amount)
def test_partial_paid_invoice_with_submitted_payment_entry(self): def test_partial_paid_invoice_with_submitted_payment_entry(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000) pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)

View File

@@ -1623,6 +1623,5 @@
"states": [], "states": [],
"timeline_field": "customer", "timeline_field": "customer",
"title_field": "title", "title_field": "title",
"track_changes": 1, "track_changes": 1
"track_seen": 1
} }

View File

@@ -4,6 +4,7 @@
import frappe import frappe
from frappe import _, msgprint, scrub, unscrub from frappe import _, msgprint, scrub, unscrub
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import get_link_to_form, now from frappe.utils import get_link_to_form, now
@@ -204,17 +205,41 @@ class POSProfile(Document):
def get_item_groups(pos_profile): def get_item_groups(pos_profile):
item_groups = [] item_groups = []
pos_profile = frappe.get_cached_doc("POS Profile", pos_profile) pos_profile = frappe.get_cached_doc("POS Profile", pos_profile)
permitted_item_groups = get_permitted_nodes("Item Group")
if pos_profile.get("item_groups"): if pos_profile.get("item_groups"):
# Get items based on the item groups defined in the POS profile # Get items based on the item groups defined in the POS profile
for data in pos_profile.get("item_groups"): for data in pos_profile.get("item_groups"):
item_groups.extend( item_groups.extend(
["%s" % frappe.db.escape(d.name) for d in get_child_nodes("Item Group", data.item_group)] [
"%s" % frappe.db.escape(d.name)
for d in get_child_nodes("Item Group", data.item_group)
if not permitted_item_groups or d.name in permitted_item_groups
]
) )
if not item_groups and permitted_item_groups:
item_groups = ["%s" % frappe.db.escape(d) for d in permitted_item_groups]
return list(set(item_groups)) return list(set(item_groups))
def get_permitted_nodes(group_type):
nodes = []
permitted_nodes = get_permitted_documents(group_type)
if not permitted_nodes:
return nodes
for node in permitted_nodes:
if frappe.db.get_value(group_type, node, "is_group"):
nodes.extend([d.name for d in get_child_nodes(group_type, node)])
else:
nodes.append(node)
return nodes
def get_child_nodes(group_type, root): def get_child_nodes(group_type, root):
lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"]) lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
return frappe.db.sql( return frappe.db.sql(

View File

@@ -2482,6 +2482,76 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
item.reload() item.reload()
self.assertEqual(item.last_purchase_rate, 0) self.assertEqual(item.last_purchase_rate, 0)
def test_adjust_incoming_rate_from_pi_with_multi_currency_and_partial_billing(self):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
pr = make_purchase_receipt(
qty=10, rate=10, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
)
pr.conversion_rate = 5300
pr.save()
pr.submit()
incoming_rate = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"incoming_rate",
)
self.assertEqual(incoming_rate, 53000) # Asserting to confirm if the default calculation is correct
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.qty = 1
pi.save()
pi.submit()
incoming_rate = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"incoming_rate",
)
# Test 1 : Incoming rate should not change as only the qty has changed and not the rate (this was not the case before)
self.assertEqual(incoming_rate, 53000)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.qty = 1
row.rate = 9
pi.save()
pi.submit()
incoming_rate = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"incoming_rate",
)
# Test 2 : Rate in new PI is lower than PR, so incoming rate should also be lower
self.assertEqual(incoming_rate, 50350)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.qty = 1
row.rate = 12
pi.save()
pi.submit()
incoming_rate = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"incoming_rate",
)
# Test 3 : Rate in new PI is higher than PR, so incoming rate should also be higher
self.assertEqual(incoming_rate, 54766.667)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
def test_opening_invoice_rounding_adjustment_validation(self): def test_opening_invoice_rounding_adjustment_validation(self):
pi = make_purchase_invoice(do_not_save=1) pi = make_purchase_invoice(do_not_save=1)
pi.items[0].rate = 99.98 pi.items[0].rate = 99.98

View File

@@ -922,9 +922,25 @@ frappe.ui.form.on("Sales Invoice", {
} }
const timesheets = await frm.events.get_timesheet_data(frm, kwargs); const timesheets = await frm.events.get_timesheet_data(frm, kwargs);
if (kwargs.item_code) {
frm.events.add_timesheet_item(frm, kwargs.item_code, timesheets);
}
return frm.events.set_timesheet_data(frm, timesheets); return frm.events.set_timesheet_data(frm, timesheets);
}, },
add_timesheet_item: function (frm, item_code, timesheets) {
const row = frm.add_child("items");
frappe.model.set_value(row.doctype, row.name, "item_code", item_code);
frappe.model.set_value(
row.doctype,
row.name,
"qty",
timesheets.reduce((a, b) => a + (b["billing_hours"] || 0.0), 0.0)
);
},
async get_timesheet_data(frm, kwargs) { async get_timesheet_data(frm, kwargs) {
return frappe return frappe
.call({ .call({
@@ -1022,6 +1038,22 @@ frappe.ui.form.on("Sales Invoice", {
fieldtype: "Date", fieldtype: "Date",
reqd: 1, reqd: 1,
}, },
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: frm.doc.customer,
has_variants: 0,
},
};
},
},
{ {
fieldtype: "Column Break", fieldtype: "Column Break",
fieldname: "col_break_1", fieldname: "col_break_1",
@@ -1046,6 +1078,7 @@ frappe.ui.form.on("Sales Invoice", {
from_time: data.from_time, from_time: data.from_time,
to_time: data.to_time, to_time: data.to_time,
project: data.project, project: data.project,
item_code: data.item_code,
}); });
d.hide(); d.hide();
}, },

View File

@@ -3,6 +3,7 @@
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2022-01-25 10:29:57.771398", "creation": "2022-01-25 10:29:57.771398",
"default_print_format": "Sales Invoice Print",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
@@ -2177,6 +2178,7 @@
"print_hide": 1 "print_hide": 1
} }
], ],
"grid_page_length": 50,
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 181, "idx": 181,
"is_submittable": 1, "is_submittable": 1,
@@ -2187,7 +2189,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2025-02-06 15:59:54.636202", "modified": "2025-03-05 17:06:59.720616",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",
@@ -2233,6 +2235,7 @@
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount", "search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",

View File

@@ -1937,13 +1937,16 @@ def is_overdue(doc, total):
"base_payment_amount" if doc.party_account_currency != doc.currency else "payment_amount" "base_payment_amount" if doc.party_account_currency != doc.currency else "payment_amount"
) )
payable_amount = sum( payable_amount = flt(
payment.get(payment_amount_field) sum(
for payment in doc.payment_schedule payment.get(payment_amount_field)
if getdate(payment.due_date) < today for payment in doc.payment_schedule
if getdate(payment.due_date) < today
),
doc.precision("outstanding_amount"),
) )
return (total - outstanding_amount) < payable_amount return flt(total - outstanding_amount, doc.precision("outstanding_amount")) < payable_amount
def get_discounting_status(sales_invoice): def get_discounting_status(sales_invoice):

View File

@@ -4246,6 +4246,31 @@ class TestSalesInvoice(FrappeTestCase):
doc = frappe.get_doc("Project", project.name) doc = frappe.get_doc("Project", project.name)
self.assertEqual(doc.total_billed_amount, si.grand_total) self.assertEqual(doc.total_billed_amount, si.grand_total)
def test_total_billed_amount_with_different_projects(self):
# This test case is for checking the scenario where project is set at document level and for **some** child items only, not all
from copy import copy
si = create_sales_invoice(do_not_submit=True)
project = frappe.new_doc("Project")
project.company = "_Test Company"
project.project_name = "Test Total Billed Amount"
project.save()
si.project = project.name
si.items.append(copy(si.items[0]))
si.items.append(copy(si.items[0]))
si.items[0].project = project.name
si.items[1].project = project.name
# Not setting project on last item
si.items[1].insert()
si.items[2].insert()
si.submit()
project.reload()
self.assertIsNone(si.items[2].project)
self.assertEqual(project.total_billed_amount, 300)
def test_pos_returns_with_party_account_currency(self): def test_pos_returns_with_party_account_currency(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return

View File

@@ -13,17 +13,15 @@
"fields": [ "fields": [
{ {
"fieldname": "voucher_type", "fieldname": "voucher_type",
"fieldtype": "Link", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Voucher Type", "label": "Voucher Type"
"options": "DocType"
}, },
{ {
"fieldname": "voucher_name", "fieldname": "voucher_name",
"fieldtype": "Dynamic Link", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Voucher Name", "label": "Voucher Name"
"options": "voucher_type"
}, },
{ {
"fieldname": "taxable_amount", "fieldname": "taxable_amount",
@@ -36,7 +34,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-01-13 13:40:41.479208", "modified": "2025-02-05 16:39:14.863698",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Tax Withheld Vouchers", "name": "Tax Withheld Vouchers",

View File

@@ -18,8 +18,8 @@ class TaxWithheldVouchers(Document):
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data
taxable_amount: DF.Currency taxable_amount: DF.Currency
voucher_name: DF.DynamicLink | None voucher_name: DF.Data | None
voucher_type: DF.Link | None voucher_type: DF.Data | None
# end: auto-generated types # end: auto-generated types
pass pass

View File

@@ -436,6 +436,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
tax_details.get("tax_withholding_category"), tax_details.get("tax_withholding_category"),
company, company,
), ),
as_dict=1,
) )
for d in journal_entries_details: for d in journal_entries_details:

View File

@@ -0,0 +1,161 @@
{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}
{% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div>
{% endif %}
{% if print_heading_template %}
{{ frappe.render_template(print_heading_template, {"doc":doc}) }}
{% else %}
{% endif %}
{%- if doc.meta.is_submittable and doc.docstatus==2-%}
<div class="text-center" document-status="cancelled">
<h4 style="margin: 0px;">{{ _("CANCELLED") }}</h4>
</div>
{%- endif -%}
{%- endmacro -%}
{% for page in layout %}
<div class="page-break">
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}
</div>
<style>
.taxes-section .order-taxes.mt-5{
margin-top: 0px !important;
}
.taxes-section .order-taxes .border-btm.pb-5{
padding-bottom: 0px !important;
}
.print-format label{
color: #74808b;
font-size: 12px;
margin-bottom: 4px;
}
</style>
{% if print_settings.repeat_header_footer %}
<div id="footer-html" class="visible-pdf">
{% if not no_letterhead and footer %}
<div class="letter-head-footer">
{{ footer }}
</div>
{% endif %}
<p class="text-center small page-number visible-pdf">
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
</p>
</div>
{% endif %}
<div class="row section-break" style="margin-bottom: 10px;">
<div class="col-xs-6 p-0">
<div class="col-xs-12 value text-uppercase"><b>{{ doc.customer }}</b></div>
<div class="col-xs-12">
{{ doc.address_display }}
</div>
<div class="col-xs-12">
{{ _("Conatct: ")+doc.contact_display if doc.contact_display else '' }}
</div>
<div class="col-xs-12">
{{ _("Mobile: ")+doc.contact_mobile if doc.contact_mobile else '' }}
</div>
</div>
<div class="col-xs-3"></div>
<div class="col-xs-3" style="padding-left: 5px;">
<div>
<div><label>{{ _("Invoice ID") }}</label></div>
<div>{{ doc.name }}</div>
</div>
<div style="margin-top: 20px;">
<div><label>{{ _("Invoice Date") }}</label></div>
<div>{{ frappe.utils.format_date(doc.posting_date) }}</div>
</div>
<div style="margin-top: 20px;">
<div><label>{{ _("Due Date") }}</label></div>
<div>{{ frappe.utils.format_date(doc.due_date) }}</div>
</div>
</div>
</div>
<div class="section-break">
<table class="table table-bordered table-condensed mb-0" style="width: 100%; border-collapse: collapse; font-size: 12px;">
<colgroup>
<col style="width: 5%">
<col style="width: 45%">
<col style="width: 10%">
<col style="width: 20%">
<col style="width: 20%">
</colgroup>
<thead>
<tr>
<th class="text-uppercase" style="text-align:center">{{ _("Sr") }}</th>
<th class="text-uppercase" style="text-align:center">{{ _("Details") }}</th>
<th class="text-uppercase" style="text-align:center">{{ _("Qty") }}</th>
<th class="text-uppercase" style="text-align:right">{{ _("Rate") }}</th>
<th class="text-uppercase" style="text-align:right">{{ _("Amount") }}</th>
</tr>
</thead>
{% for item in doc.items %}
<tr>
<td style="text-align:center">{{ loop.index }}</td>
<td>
<b>{{ item.item_code }}: {{ item.item_name }}</b>
{% if (item.description != item.item_name) %}
<br>{{ item.description }}
{% endif %}
</td>
<td style="text-align: center;">
{{ item.get_formatted("qty", 0) }}
{{ item.get_formatted("uom", 0) }}
</td>
<td style="text-align: right;">{{ item.get_formatted("net_rate", doc) }}</td>
<td style="text-align: right;">{{ item.get_formatted("net_amount", doc) }}</td>
</tr>
{% endfor %}
</table>
<!-- total -->
<div class="row">
<div class="col-xs-6">
<div>
<label>{{ _("Amount in Words") }}</label>
{{ doc.in_words }}
</div>
<div style="margin-top: 20px;">
<label>{{ _("Payment Status") }}</label>
{{ doc.status }}
</div>
</div>
<div class="col-xs-6">
<div class="row section-break">
<div class="col-xs-7"><div>{{ _("Sub Total") }}</div></div>
<div class="col-xs-5" style="text-align: right;">{{ doc.get_formatted("net_total", doc) }}</div>
</div>
<div>
{% for d in doc.taxes %}
{% if d.tax_amount %}
<div class="row">
<div class="col-xs-8"><div>{{ _(d.description) }}</div></div>
<div class="col-xs-4" style="text-align: right;">{{ d.get_formatted("tax_amount") }}</div>
</div>
{% endif %}
{% endfor %}
</div>
<div class="row">
<div class="col-xs-7"><div>{{ _("Total") }}</div></div>
<div class="col-xs-5" style="text-align: right;">{{ doc.get_formatted("grand_total", doc) }}</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="row important data-field">
<div class="col-xs-12"><label>{{ _("Terms and Conditions") }}: </label></div>
<div class="col-xs-12">{{ doc.terms if doc.terms else '' }}</div>
</div>
</div>
</div>
</div>
{% endfor %}

View File

@@ -0,0 +1,32 @@
{
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2025-01-22 16:23:51.012200",
"css": "",
"custom_format": 0,
"default_print_language": "en",
"disabled": 0,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "",
"font_size": 14,
"idx": 0,
"line_breaks": 0,
"margin_bottom": 0.0,
"margin_left": 0.0,
"margin_right": 0.0,
"margin_top": 0.0,
"modified": "2025-01-22 16:23:51.012200",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Print",
"owner": "Administrator",
"page_number": "Hide",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

View File

@@ -38,6 +38,23 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(data[1][0].get("outstanding"), 300) self.assertEqual(data[1][0].get("outstanding"), 300)
self.assertEqual(data[1][0].get("currency"), "USD") self.assertEqual(data[1][0].get("currency"), "USD")
def test_account_payable_for_debit_note(self):
pi = self.create_purchase_invoice(do_not_submit=True)
pi.is_return = 1
pi.items[0].qty = -1
pi = pi.save().submit()
filters = {
"company": self.company,
"party_type": "Supplier",
"party": [self.supplier],
"report_date": today(),
"range": "30, 60, 90, 120",
}
data = execute(filters)
self.assertEqual(data[1][0].get("invoiced"), 300)
def create_purchase_invoice(self, do_not_submit=False): def create_purchase_invoice(self, do_not_submit=False):
frappe.set_user("Administrator") frappe.set_user("Administrator")
pi = make_purchase_invoice( pi = make_purchase_invoice(

View File

@@ -267,6 +267,18 @@ class ReceivablePayableReport:
row.invoiced_in_account_currency += amount_in_account_currency row.invoiced_in_account_currency += amount_in_account_currency
else: else:
if self.is_invoice(ple): if self.is_invoice(ple):
# when invoice has is_return marked
if self.invoice_details.get(row.voucher_no, {}).get("is_return"):
# for Credit Note
if row.voucher_type == "Sales Invoice":
row.credit_note -= amount
row.credit_note_in_account_currency -= amount_in_account_currency
# for Debit Note
else:
row.invoiced -= amount
row.invoiced_in_account_currency -= amount_in_account_currency
return
if row.voucher_no == ple.voucher_no == ple.against_voucher_no: if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
row.paid -= amount row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency row.paid_in_account_currency -= amount_in_account_currency
@@ -421,7 +433,7 @@ class ReceivablePayableReport:
# nosemgrep # nosemgrep
si_list = frappe.db.sql( si_list = frappe.db.sql(
""" """
select name, due_date, po_no select name, due_date, po_no, is_return
from `tabSales Invoice` from `tabSales Invoice`
where posting_date <= %s where posting_date <= %s
and company = %s and company = %s
@@ -453,7 +465,7 @@ class ReceivablePayableReport:
# nosemgrep # nosemgrep
for pi in frappe.db.sql( for pi in frappe.db.sql(
""" """
select name, due_date, bill_no, bill_date select name, due_date, bill_no, bill_date, is_return
from `tabPurchase Invoice` from `tabPurchase Invoice`
where where
posting_date <= %s posting_date <= %s

View File

@@ -204,7 +204,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
expected_data_after_credit_note = [ expected_data_after_credit_note = [
[100.0, 100.0, 40.0, 0.0, 60.0, si.name], [100.0, 100.0, 40.0, 0.0, 60.0, si.name],
[0, 0, 100.0, 0.0, -100.0, cr_note.name], [0, 0, 0, 100.0, -100.0, cr_note.name],
] ]
self.assertEqual(len(report[1]), 2) self.assertEqual(len(report[1]), 2)
si_row = next( si_row = next(
@@ -478,13 +478,19 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
report = execute(filters)[1] report = execute(filters)[1]
self.assertEqual(len(report), 2) self.assertEqual(len(report), 2)
expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]} expected_data = {sr.name: [0.0, 10.0, -10.0, 0.0, -10], si.name: [100.0, 0.0, 100.0, 10.0, 90.0]}
rows = report[:2] rows = report[:2]
for row in rows: for row in rows:
self.assertEqual( self.assertEqual(
expected_data[row.voucher_no], expected_data[row.voucher_no],
[row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount], [
row.invoiced or row.paid,
row.credit_note,
row.outstanding,
row.remaining_balance,
row.future_amount,
],
) )
pe.cancel() pe.cancel()

View File

@@ -50,6 +50,7 @@ def get_group_by_asset_category_data(filters):
flt(row.accumulated_depreciation_as_on_from_date) flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period) + flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period) - flt(row.depreciation_eliminated_during_the_period)
- flt(row.depreciation_eliminated_via_reversal)
) )
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt( row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
@@ -247,6 +248,7 @@ def get_group_by_asset_data(filters):
flt(row.accumulated_depreciation_as_on_from_date) flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period) + flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period) - flt(row.depreciation_eliminated_during_the_period)
- flt(row.depreciation_eliminated_via_reversal)
) )
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt( row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
@@ -276,6 +278,7 @@ def get_assets_for_grouped_by_category(filters):
f""" f"""
SELECT results.asset_category, SELECT results.asset_category,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category, from (SELECT a.asset_category,
@@ -284,6 +287,11 @@ def get_assets_for_grouped_by_category(filters):
else else
0 0
end), 0) as accumulated_depreciation_as_on_from_date, end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
gle.credit
else
0
end), 0) as depreciation_eliminated_via_reversal,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit gle.debit
@@ -307,7 +315,6 @@ def get_assets_for_grouped_by_category(filters):
a.docstatus=1 a.docstatus=1
and a.company=%(company)s and a.company=%(company)s
and a.purchase_date <= %(to_date)s and a.purchase_date <= %(to_date)s
and gle.debit != 0
and gle.is_cancelled = 0 and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{condition} {finance_book_filter} {condition} {finance_book_filter}
@@ -319,6 +326,7 @@ def get_assets_for_grouped_by_category(filters):
else else
a.opening_accumulated_depreciation a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date, end), 0) as accumulated_depreciation_as_on_from_date,
0 as depreciation_eliminated_via_reversal,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation a.opening_accumulated_depreciation
else else
@@ -354,6 +362,7 @@ def get_assets_for_grouped_by_asset(filters):
f""" f"""
SELECT results.name as asset, SELECT results.name as asset,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.name as name, from (SELECT a.name as name,
@@ -362,6 +371,11 @@ def get_assets_for_grouped_by_asset(filters):
else else
0 0
end), 0) as accumulated_depreciation_as_on_from_date, end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
gle.credit
else
0
end), 0) as depreciation_eliminated_via_reversal,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit gle.debit
@@ -385,7 +399,6 @@ def get_assets_for_grouped_by_asset(filters):
a.docstatus=1 a.docstatus=1
and a.company=%(company)s and a.company=%(company)s
and a.purchase_date <= %(to_date)s and a.purchase_date <= %(to_date)s
and gle.debit != 0
and gle.is_cancelled = 0 and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{finance_book_filter} {condition} {finance_book_filter} {condition}
@@ -397,6 +410,7 @@ def get_assets_for_grouped_by_asset(filters):
else else
a.opening_accumulated_depreciation a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date, end), 0) as accumulated_depreciation_as_on_from_date,
0 as depreciation_as_on_from_date_credit,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation a.opening_accumulated_depreciation
else else
@@ -503,6 +517,12 @@ def get_columns(filters):
"fieldtype": "Currency", "fieldtype": "Currency",
"width": 270, "width": 270,
}, },
{
"label": _("Depreciation eliminated via reversal"),
"fieldname": "depreciation_eliminated_via_reversal",
"fieldtype": "Currency",
"width": 270,
},
{ {
"label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date), "label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "net_asset_value_as_on_from_date", "fieldname": "net_asset_value_as_on_from_date",

View File

@@ -27,6 +27,7 @@ def get_report_filters(report_filters):
["Purchase Invoice", "docstatus", "=", 1], ["Purchase Invoice", "docstatus", "=", 1],
["Purchase Invoice", "per_received", "<", 100], ["Purchase Invoice", "per_received", "<", 100],
["Purchase Invoice", "update_stock", "=", 0], ["Purchase Invoice", "update_stock", "=", 0],
["Purchase Invoice", "is_opening", "!=", "Yes"],
] ]
if report_filters.get("purchase_invoice"): if report_filters.get("purchase_invoice"):

View File

@@ -263,6 +263,7 @@ def get_actual_details(name, filters):
and ba.account=gl.account and ba.account=gl.account
and b.{budget_against} = gl.{budget_against} and b.{budget_against} = gl.{budget_against}
and gl.fiscal_year between %s and %s and gl.fiscal_year between %s and %s
and gl.is_cancelled = 0
and b.{budget_against} = %s and b.{budget_against} = %s
and exists( and exists(
select select

View File

@@ -307,6 +307,7 @@ class Deferred_Revenue_and_Expense_Report:
.where( .where(
(inv.docstatus == 1) (inv.docstatus == 1)
& (deferred_flag_field == 1) & (deferred_flag_field == 1)
& (inv.company == self.filters.company)
& ( & (
( (
(self.period_list[0].from_date >= inv_item.service_start_date) (self.period_list[0].from_date >= inv_item.service_start_date)

View File

@@ -2,5 +2,27 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.query_reports["Delivered Items To Be Billed"] = { frappe.query_reports["Delivered Items To Be Billed"] = {
filters: [], filters: [
{
label: __("Company"),
fieldname: "company",
fieldtype: "Link",
options: "Company",
reqd: 1,
default: frappe.defaults.get_default("Company"),
},
{
label: __("As on Date"),
fieldname: "posting_date",
fieldtype: "Date",
reqd: 1,
default: frappe.datetime.get_today(),
},
{
label: __("Delivery Note"),
fieldname: "delivery_note",
fieldtype: "Link",
options: "Delivery Note",
},
],
}; };

View File

@@ -3,6 +3,7 @@
from frappe import _ from frappe import _
from pypika import Order
from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data
@@ -10,7 +11,7 @@ from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_d
def execute(filters=None): def execute(filters=None):
columns = get_column() columns = get_column()
args = get_args() args = get_args()
data = get_ordered_to_be_billed_data(args) data = get_ordered_to_be_billed_data(args, filters)
return columns, data return columns, data
@@ -76,13 +77,6 @@ def get_column():
"options": "Project", "options": "Project",
"width": 120, "width": 120,
}, },
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"width": 120,
},
] ]
@@ -92,5 +86,6 @@ def get_args():
"party": "customer", "party": "customer",
"date": "posting_date", "date": "posting_date",
"order": "name", "order": "name",
"order_by": "desc", "order_by": Order.desc,
"reference_field": "delivery_note",
} }

View File

@@ -534,6 +534,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
for dim in accounting_dimensions: for dim in accounting_dimensions:
keylist.append(gle.get(dim)) keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center")) keylist.append(gle.get("cost_center"))
keylist.append(gle.get("project"))
key = tuple(keylist) key = tuple(keylist)
if key not in consolidated_gle: if key not in consolidated_gle:
@@ -679,10 +680,11 @@ def get_columns(filters):
{"label": _("Against Account"), "fieldname": "against", "width": 120}, {"label": _("Against Account"), "fieldname": "against", "width": 120},
{"label": _("Party Type"), "fieldname": "party_type", "width": 100}, {"label": _("Party Type"), "fieldname": "party_type", "width": 100},
{"label": _("Party"), "fieldname": "party", "width": 100}, {"label": _("Party"), "fieldname": "party", "width": 100},
{"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100},
] ]
if filters.get("include_dimensions"): if filters.get("include_dimensions"):
columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100})
for dim in get_accounting_dimensions(as_list=False): for dim in get_accounting_dimensions(as_list=False):
columns.append( columns.append(
{"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100} {"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100}

View File

@@ -4,11 +4,12 @@
import frappe import frappe
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.query_builder.functions import IfNull, Round
from erpnext import get_default_currency from erpnext import get_default_currency
def get_ordered_to_be_billed_data(args): def get_ordered_to_be_billed_data(args, filters=None):
doctype, party = args.get("doctype"), args.get("party") doctype, party = args.get("doctype"), args.get("party")
child_tab = doctype + " Item" child_tab = doctype + " Item"
precision = ( precision = (
@@ -18,47 +19,57 @@ def get_ordered_to_be_billed_data(args):
or 2 or 2
) )
project_field = get_project_field(doctype, party) doctype = frappe.qb.DocType(doctype)
child_doctype = frappe.qb.DocType(child_tab)
return frappe.db.sql( docname = filters.get(args.get("reference_field"), None)
""" project_field = get_project_field(doctype, child_doctype, party)
Select
`{parent_tab}`.name, `{parent_tab}`.{date_field}, query = (
`{parent_tab}`.{party}, `{parent_tab}`.{party}_name, frappe.qb.from_(doctype)
`{child_tab}`.item_code, .inner_join(child_doctype)
`{child_tab}`.base_amount, .on(doctype.name == child_doctype.parent)
(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)), .select(
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0)), doctype.name,
(`{child_tab}`.base_amount - doctype[args.get("date")].as_("date"),
(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)) - doctype[party],
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))), doctype[party + "_name"],
`{child_tab}`.item_name, `{child_tab}`.description, child_doctype.item_code,
{project_field}, `{parent_tab}`.company child_doctype.base_amount.as_("amount"),
from (child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1)).as_("billed_amount"),
`{parent_tab}`, `{child_tab}` (child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0)).as_("returned_amount"),
where (
`{parent_tab}`.name = `{child_tab}`.parent and `{parent_tab}`.docstatus = 1 child_doctype.base_amount
and `{parent_tab}`.status not in ('Closed', 'Completed') - (child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1))
and `{child_tab}`.amount > 0 - (child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0))
and (`{child_tab}`.base_amount - ).as_("pending_amount"),
round(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) - child_doctype.item_name,
(`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0 child_doctype.description,
order by project_field,
`{parent_tab}`.{order} {order_by}
""".format(
parent_tab="tab" + doctype,
child_tab="tab" + child_tab,
precision=precision,
party=party,
date_field=args.get("date"),
project_field=project_field,
order=args.get("order"),
order_by=args.get("order_by"),
) )
.where(
(doctype.docstatus == 1)
& (doctype.status.notin(["Closed", "Completed"]))
& (doctype.company == filters.get("company"))
& (doctype.posting_date <= filters.get("posting_date"))
& (child_doctype.amount > 0)
& (
child_doctype.base_amount
- Round(child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1), precision)
- (child_doctype.base_rate * IfNull(child_doctype.returned_qty, 0))
)
> 0
)
.orderby(doctype[args.get("order")], order=args.get("order_by"))
) )
if docname:
query = query.where(doctype.name == docname)
def get_project_field(doctype, party): return query.run(as_dict=True)
def get_project_field(doctype, child_doctype, party):
if party == "supplier": if party == "supplier":
doctype = doctype + " Item" return child_doctype.project
return "`tab%s`.project" % (doctype) return doctype.project

View File

@@ -2,5 +2,27 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.query_reports["Received Items To Be Billed"] = { frappe.query_reports["Received Items To Be Billed"] = {
filters: [], filters: [
{
label: __("Company"),
fieldname: "company",
fieldtype: "Link",
options: "Company",
reqd: 1,
default: frappe.defaults.get_default("Company"),
},
{
label: __("As on Date"),
fieldname: "posting_date",
fieldtype: "Date",
reqd: 1,
default: frappe.datetime.get_today(),
},
{
label: __("Purchase Receipt"),
fieldname: "purchase_receipt",
fieldtype: "Link",
options: "Purchase Receipt",
},
],
}; };

View File

@@ -3,6 +3,7 @@
from frappe import _ from frappe import _
from pypika import Order
from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_data
@@ -10,7 +11,7 @@ from erpnext.accounts.report.non_billed_report import get_ordered_to_be_billed_d
def execute(filters=None): def execute(filters=None):
columns = get_column() columns = get_column()
args = get_args() args = get_args()
data = get_ordered_to_be_billed_data(args) data = get_ordered_to_be_billed_data(args, filters)
return columns, data return columns, data
@@ -76,13 +77,6 @@ def get_column():
"options": "Project", "options": "Project",
"width": 120, "width": 120,
}, },
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"width": 120,
},
] ]
@@ -92,5 +86,6 @@ def get_args():
"party": "supplier", "party": "supplier",
"date": "posting_date", "date": "posting_date",
"order": "name", "order": "name",
"order_by": "desc", "order_by": Order.desc,
"reference_field": "purchase_receipt",
} }

View File

@@ -1417,7 +1417,7 @@ def repost_gle_for_stock_vouchers(
if not warehouse_account: if not warehouse_account:
warehouse_account = get_warehouse_account_map(company) warehouse_account = get_warehouse_account_map(company)
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers) stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers, company=company)
if repost_doc and repost_doc.gl_reposting_index: if repost_doc and repost_doc.gl_reposting_index:
# Restore progress # Restore progress
stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :] stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :]
@@ -1470,7 +1470,9 @@ def _delete_accounting_ledger_entries(voucher_type, voucher_no):
_delete_pl_entries(voucher_type, voucher_no) _delete_pl_entries(voucher_type, voucher_no)
def sort_stock_vouchers_by_posting_date(stock_vouchers: list[tuple[str, str]]) -> list[tuple[str, str]]: def sort_stock_vouchers_by_posting_date(
stock_vouchers: list[tuple[str, str]], company=None
) -> list[tuple[str, str]]:
sle = frappe.qb.DocType("Stock Ledger Entry") sle = frappe.qb.DocType("Stock Ledger Entry")
voucher_nos = [v[1] for v in stock_vouchers] voucher_nos = [v[1] for v in stock_vouchers]
@@ -1481,7 +1483,12 @@ def sort_stock_vouchers_by_posting_date(stock_vouchers: list[tuple[str, str]]) -
.groupby(sle.voucher_type, sle.voucher_no) .groupby(sle.voucher_type, sle.voucher_no)
.orderby(sle.posting_datetime) .orderby(sle.posting_datetime)
.orderby(sle.creation) .orderby(sle.creation)
).run(as_dict=True) )
if company:
sles = sles.where(sle.company == company)
sles = sles.run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers) unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers)

View File

@@ -93,7 +93,7 @@
}, },
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 1,
"label": "Accounts Payable", "label": "Accounts Payable",
"link_count": 0, "link_count": 0,
"link_to": "Accounts Payable", "link_to": "Accounts Payable",
@@ -103,7 +103,7 @@
}, },
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 1,
"label": "Accounts Payable Summary", "label": "Accounts Payable Summary",
"link_count": 0, "link_count": 0,
"link_to": "Accounts Payable Summary", "link_to": "Accounts Payable Summary",
@@ -113,7 +113,7 @@
}, },
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 1,
"label": "Purchase Register", "label": "Purchase Register",
"link_count": 0, "link_count": 0,
"link_to": "Purchase Register", "link_to": "Purchase Register",
@@ -123,7 +123,7 @@
}, },
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 1,
"label": "Item-wise Purchase Register", "label": "Item-wise Purchase Register",
"link_count": 0, "link_count": 0,
"link_to": "Item-wise Purchase Register", "link_to": "Item-wise Purchase Register",
@@ -133,7 +133,7 @@
}, },
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 1,
"label": "Purchase Order Analysis", "label": "Purchase Order Analysis",
"link_count": 0, "link_count": 0,
"link_to": "Purchase Order Analysis", "link_to": "Purchase Order Analysis",
@@ -143,7 +143,7 @@
}, },
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 1,
"label": "Received Items To Be Billed", "label": "Received Items To Be Billed",
"link_count": 0, "link_count": 0,
"link_to": "Received Items To Be Billed", "link_to": "Received Items To Be Billed",
@@ -153,7 +153,7 @@
}, },
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 1,
"label": "Supplier Ledger Summary", "label": "Supplier Ledger Summary",
"link_count": 0, "link_count": 0,
"link_to": "Supplier Ledger Summary", "link_to": "Supplier Ledger Summary",

View File

@@ -225,7 +225,7 @@
{ {
"fieldname": "gross_purchase_amount", "fieldname": "gross_purchase_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Gross Purchase Amount", "label": "Net Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)", "mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency" "options": "Company:company:default_currency"
}, },
@@ -592,7 +592,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2025-02-11 16:01:56.140904", "modified": "2025-02-20 14:09:05.421913",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@@ -143,14 +143,19 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
} }
} }
set_consumed_stock_items_tagged_to_wip_composite_asset(asset) { set_consumed_stock_items_tagged_to_wip_composite_asset(target_asset) {
var me = this; var me = this;
if (asset) { if (target_asset) {
return me.frm.call({ return me.frm.call({
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset", method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset",
args: { args: {
asset: asset, params: {
target_asset: target_asset,
finance_book: me.frm.doc.finance_book,
posting_date: me.frm.doc.posting_date,
posting_time: me.frm.doc.posting_time,
},
}, },
callback: function (r) { callback: function (r) {
if (!r.exc && r.message) { if (!r.exc && r.message) {

View File

@@ -856,7 +856,10 @@ def get_service_item_details(args):
@frappe.whitelist() @frappe.whitelist()
def get_items_tagged_to_wip_composite_asset(asset): def get_items_tagged_to_wip_composite_asset(params):
if isinstance(params, str):
params = json.loads(params)
fields = [ fields = [
"item_code", "item_code",
"item_name", "item_name",
@@ -871,25 +874,66 @@ def get_items_tagged_to_wip_composite_asset(asset):
"amount", "amount",
"is_fixed_asset", "is_fixed_asset",
"parent", "parent",
"name",
] ]
pr_items = frappe.get_all( pr_items = frappe.get_all(
"Purchase Receipt Item", filters={"wip_composite_asset": asset, "docstatus": 1}, fields=fields "Purchase Receipt Item",
filters={"wip_composite_asset": params.get("target_asset"), "docstatus": 1},
fields=fields,
) )
stock_items = [] stock_items = []
asset_items = [] asset_items = []
for d in pr_items: for d in pr_items:
if not d.is_fixed_asset: if not d.is_fixed_asset:
stock_items.append(frappe._dict(d)) stock_item = process_stock_item(d)
if stock_item:
stock_items.append(stock_item)
else: else:
asset_details = frappe.db.get_value( asset_item = process_fixed_asset(d)
"Asset", if asset_item:
{"item_code": d.item_code, "purchase_receipt": d.parent}, asset_items.append(asset_item)
["name as asset", "asset_name"],
as_dict=1,
)
d.update(asset_details)
asset_items.append(frappe._dict(d))
return stock_items, asset_items return stock_items, asset_items
def process_stock_item(d):
stock_capitalized = frappe.db.exists(
"Asset Capitalization Stock Item",
{
"purchase_receipt_item": d.name,
"parentfield": "stock_items",
"parenttype": "Asset Capitalization",
"docstatus": 1,
},
)
if stock_capitalized:
return None
stock_item_data = frappe._dict(d)
stock_item_data.purchase_receipt_item = d.name
return stock_item_data
def process_fixed_asset(d):
asset_details = frappe.db.get_value(
"Asset",
{
"item_code": d.item_code,
"purchase_receipt": d.parent,
"status": ("not in", ["Draft", "Scrapped", "Sold", "Capitalized"]),
},
["name as asset", "asset_name", "company"],
as_dict=1,
)
if asset_details:
asset_details.update(d)
asset_details.update(get_consumed_asset_details(asset_details))
d.update(asset_details)
return frappe._dict(d)
return None

View File

@@ -10,12 +10,13 @@
"column_break_3", "column_break_3",
"warehouse", "warehouse",
"section_break_6", "section_break_6",
"purchase_receipt_item",
"stock_qty", "stock_qty",
"stock_uom",
"actual_qty", "actual_qty",
"column_break_9", "column_break_9",
"valuation_rate", "valuation_rate",
"amount", "amount",
"stock_uom",
"batch_and_serial_no_section", "batch_and_serial_no_section",
"serial_and_batch_bundle", "serial_and_batch_bundle",
"use_serial_batch_fields", "use_serial_batch_fields",
@@ -53,14 +54,14 @@
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Qty and Rate" "label": "Purchase Details"
}, },
{ {
"columns": 1, "columns": 1,
"fieldname": "stock_qty", "fieldname": "stock_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Qty", "label": "Quantity",
"non_negative": 1 "non_negative": 1
}, },
{ {
@@ -172,18 +173,26 @@
{ {
"fieldname": "column_break_mbuv", "fieldname": "column_break_mbuv",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "purchase_receipt_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Purchase Receipt Item"
} }
], ],
"grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-06-26 17:06:22.564438", "modified": "2025-03-05 12:46:01.074742",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Capitalization Stock Item", "name": "Asset Capitalization Stock Item",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1

View File

@@ -23,6 +23,7 @@ class AssetCapitalizationStockItem(Document):
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data
purchase_receipt_item: DF.Data | None
serial_and_batch_bundle: DF.Link | None serial_and_batch_bundle: DF.Link | None
serial_no: DF.Text | None serial_no: DF.Text | None
stock_qty: DF.Float stock_qty: DF.Float

View File

@@ -404,7 +404,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
); );
} }
} else { } else {
if (!doc.items.every((item) => item.qty == item.sco_qty)) { if (!doc.items.every((item) => item.qty == item.subcontracted_quantity)) {
this.frm.add_custom_button( this.frm.add_custom_button(
__("Subcontracting Order"), __("Subcontracting Order"),
() => { () => {

View File

@@ -898,7 +898,7 @@ def is_po_fully_subcontracted(po_name):
query = ( query = (
frappe.qb.from_(table) frappe.qb.from_(table)
.select(table.name) .select(table.name)
.where((table.parent == po_name) & (table.qty != table.sco_qty)) .where((table.parent == po_name) & (table.qty != table.subcontracted_quantity))
) )
return not query.run(as_dict=True) return not query.run(as_dict=True)
@@ -945,7 +945,7 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
"material_request_item": "material_request_item", "material_request_item": "material_request_item",
}, },
"field_no_map": ["qty", "fg_item_qty", "amount"], "field_no_map": ["qty", "fg_item_qty", "amount"],
"condition": lambda item: item.qty != item.sco_qty, "condition": lambda item: item.qty != item.subcontracted_quantity,
}, },
}, },
target_doc, target_doc,

View File

@@ -1076,9 +1076,9 @@ class TestPurchaseOrder(FrappeTestCase):
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly # Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
po.reload() po.reload()
self.assertEqual(po.items[0].sco_qty, 5) self.assertEqual(po.items[0].subcontracted_quantity, 5)
self.assertEqual(po.items[1].sco_qty, 0) self.assertEqual(po.items[1].subcontracted_quantity, 0)
self.assertEqual(po.items[2].sco_qty, 12.5) self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
# Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity # Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity
self.assertEqual(sco.items[0].amount, 2000) self.assertEqual(sco.items[0].amount, 2000)
@@ -1114,10 +1114,10 @@ class TestPurchaseOrder(FrappeTestCase):
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled # Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
po.reload() po.reload()
self.assertEqual(po.items[2].sco_qty, 25) self.assertEqual(po.items[2].subcontracted_quantity, 25)
sco.cancel() sco.cancel()
po.reload() po.reload()
self.assertEqual(po.items[2].sco_qty, 12.5) self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
sco = make_subcontracting_order(po.name) sco = make_subcontracting_order(po.name)
sco.save() sco.save()

View File

@@ -26,7 +26,7 @@
"quantity_and_rate", "quantity_and_rate",
"qty", "qty",
"stock_uom", "stock_uom",
"sco_qty", "subcontracted_quantity",
"col_break2", "col_break2",
"uom", "uom",
"conversion_factor", "conversion_factor",
@@ -913,7 +913,7 @@
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "sco_qty", "fieldname": "subcontracted_quantity",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Subcontracted Quantity", "label": "Subcontracted Quantity",
"no_copy": 1, "no_copy": 1,
@@ -921,11 +921,12 @@
"read_only": 1 "read_only": 1
} }
], ],
"grid_page_length": 50,
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-02-18 12:35:04.432636", "modified": "2025-03-02 16:58:26.059601",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",
@@ -933,6 +934,7 @@
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "item_name", "search_fields": "item_name",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",

View File

@@ -80,10 +80,10 @@ class PurchaseOrderItem(Document):
sales_order_item: DF.Data | None sales_order_item: DF.Data | None
sales_order_packed_item: DF.Data | None sales_order_packed_item: DF.Data | None
schedule_date: DF.Date schedule_date: DF.Date
sco_qty: DF.Float
stock_qty: DF.Float stock_qty: DF.Float
stock_uom: DF.Link stock_uom: DF.Link
stock_uom_rate: DF.Currency stock_uom_rate: DF.Currency
subcontracted_quantity: DF.Float
supplier_part_no: DF.Data | None supplier_part_no: DF.Data | None
supplier_quotation: DF.Link | None supplier_quotation: DF.Link | None
supplier_quotation_item: DF.Link | None supplier_quotation_item: DF.Link | None

View File

@@ -19,6 +19,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80", width: "80",
reqd: 1, reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
on_change: (report) => {
report.set_filter_value("name", []);
report.refresh();
},
}, },
{ {
fieldname: "to_date", fieldname: "to_date",
@@ -27,6 +31,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80", width: "80",
reqd: 1, reqd: 1,
default: frappe.datetime.get_today(), default: frappe.datetime.get_today(),
on_change: (report) => {
report.set_filter_value("name", []);
report.refresh();
},
}, },
{ {
fieldname: "project", fieldname: "project",
@@ -38,13 +46,17 @@ frappe.query_reports["Purchase Order Analysis"] = {
{ {
fieldname: "name", fieldname: "name",
label: __("Purchase Order"), label: __("Purchase Order"),
fieldtype: "Link", fieldtype: "MultiSelectList",
width: "80", width: "80",
options: "Purchase Order", options: "Purchase Order",
get_query: () => { get_data: function (txt) {
return { let filters = { docstatus: 1 };
filters: { docstatus: 1 },
}; const from_date = frappe.query_report.get_filter_value("from_date");
const to_date = frappe.query_report.get_filter_value("to_date");
if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
return frappe.db.get_link_options("Purchase Order", txt, filters);
}, },
}, },
{ {
@@ -52,9 +64,16 @@ frappe.query_reports["Purchase Order Analysis"] = {
label: __("Status"), label: __("Status"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
width: "80", width: "80",
options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"], options: ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed", "Closed"],
get_data: function (txt) { get_data: function (txt) {
let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"]; let status = [
"To Pay",
"To Bill",
"To Receive",
"To Receive and Bill",
"Completed",
"Closed",
];
let options = []; let options = [];
for (let option of status) { for (let option of status) {
options.push({ options.push({

View File

@@ -70,14 +70,16 @@ def get_data(filters):
po.company, po.company,
po_item.name, po_item.name,
) )
.where((po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1)) .where((po_item.parent == po.name) & (po.status.notin(("Stopped", "On Hold"))) & (po.docstatus == 1))
.groupby(po_item.name) .groupby(po_item.name)
.orderby(po.transaction_date) .orderby(po.transaction_date)
) )
for field in ("company", "name"): if filters.get("company"):
if filters.get(field): query = query.where(po.company == filters.get("company"))
query = query.where(po[field] == filters.get(field))
if filters.get("name"):
query = query.where(po.name.isin(filters.get("name")))
if filters.get("from_date") and filters.get("to_date"): if filters.get("from_date") and filters.get("to_date"):
query = query.where(po.transaction_date.between(filters.get("from_date"), filters.get("to_date"))) query = query.where(po.transaction_date.between(filters.get("from_date"), filters.get("to_date")))

View File

@@ -194,6 +194,14 @@ class AccountsController(TransactionBase):
self.set_incoming_rate() self.set_incoming_rate()
self.init_internal_values() self.init_internal_values()
# Need to set taxes based on taxes_and_charges template
# before calculating taxes and totals
if self.meta.get_field("taxes_and_charges"):
self.validate_enabled_taxes_and_charges()
self.validate_tax_account_company()
self.set_taxes_and_charges()
if self.meta.get_field("currency"): if self.meta.get_field("currency"):
self.calculate_taxes_and_totals() self.calculate_taxes_and_totals()
@@ -204,10 +212,6 @@ class AccountsController(TransactionBase):
self.validate_all_documents_schedule() self.validate_all_documents_schedule()
if self.meta.get_field("taxes_and_charges"):
self.validate_enabled_taxes_and_charges()
self.validate_tax_account_company()
self.validate_party() self.validate_party()
self.validate_currency() self.validate_currency()
self.validate_party_account_currency() self.validate_party_account_currency()
@@ -252,8 +256,6 @@ class AccountsController(TransactionBase):
self.validate_deferred_income_expense_account() self.validate_deferred_income_expense_account()
self.set_inter_company_account() self.set_inter_company_account()
self.set_taxes_and_charges()
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":
self.calculate_paid_amount() self.calculate_paid_amount()
# apply tax withholding only if checked and applicable # apply tax withholding only if checked and applicable
@@ -821,11 +823,15 @@ class AccountsController(TransactionBase):
and item.get("use_serial_batch_fields") and item.get("use_serial_batch_fields")
) )
): ):
if fieldname == "batch_no" and not item.batch_no and not item.is_free_item:
item.set("rate", ret.get("rate"))
item.set("price_list_rate", ret.get("price_list_rate"))
item.set(fieldname, value) item.set(fieldname, value)
if fieldname == "batch_no" and item.batch_no and not item.is_free_item:
if ret.get("rate"):
item.set("rate", ret.get("rate"))
if not item.get("price_list_rate") and ret.get("price_list_rate"):
item.set("price_list_rate", ret.get("price_list_rate"))
elif fieldname in ["cost_center", "conversion_factor"] and not item.get( elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
fieldname fieldname
): ):

View File

@@ -333,7 +333,7 @@ class BuyingController(SubcontractingController):
net_rate net_rate
+ item.item_tax_amount + item.item_tax_amount
+ flt(item.landed_cost_voucher_amount) + flt(item.landed_cost_voucher_amount)
+ flt(item.get("rate_difference_with_purchase_invoice")) + flt(item.get("amount_difference_with_purchase_invoice"))
) / qty_in_stock_uom ) / qty_in_stock_uom
else: else:
item.valuation_rate = 0.0 item.valuation_rate = 0.0

View File

@@ -1234,7 +1234,7 @@ class StockController(AccountsController):
child_tab.item_code, child_tab.item_code,
child_tab.qty, child_tab.qty,
) )
.where(parent_tab.docstatus < 2) .where(parent_tab.docstatus == 1)
) )
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":

View File

@@ -104,18 +104,18 @@ class SubcontractingController(StockController):
) )
if ( if (
self.doctype == "Subcontracting Order" and not item.sc_conversion_factor self.doctype == "Subcontracting Order" and not item.subcontracting_conversion_factor
): # this condition will only be true if user has recently updated from develop branch ): # this condition will only be true if user has recently updated from develop branch
service_item_qty = frappe.get_value( service_item_qty = frappe.get_value(
"Subcontracting Order Service Item", "Subcontracting Order Service Item",
filters={"purchase_order_item": item.purchase_order_item, "parent": self.name}, filters={"purchase_order_item": item.purchase_order_item, "parent": self.name},
fieldname=["qty"], fieldname=["qty"],
) )
item.sc_conversion_factor = service_item_qty / item.qty item.subcontracting_conversion_factor = service_item_qty / item.qty
if self.doctype not in "Subcontracting Receipt" and item.qty > flt( if self.doctype not in "Subcontracting Receipt" and item.qty > flt(
get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item) get_pending_subcontracted_quantity(self.purchase_order).get(item.purchase_order_item)
/ item.sc_conversion_factor, / item.subcontracting_conversion_factor,
frappe.get_precision("Purchase Order Item", "qty"), frappe.get_precision("Purchase Order Item", "qty"),
): ):
frappe.throw( frappe.throw(
@@ -1132,10 +1132,14 @@ def get_item_details(items):
return item_details return item_details
def get_pending_sco_qty(po_name): def get_pending_subcontracted_quantity(po_name):
table = frappe.qb.DocType("Purchase Order Item") table = frappe.qb.DocType("Purchase Order Item")
query = frappe.qb.from_(table).select(table.name, table.qty, table.sco_qty).where(table.parent == po_name) query = (
return {item.name: item.qty - item.sco_qty for item in query.run(as_dict=True)} frappe.qb.from_(table)
.select(table.name, table.qty, table.subcontracted_quantity)
.where(table.parent == po_name)
)
return {item.name: item.qty - item.subcontracted_quantity for item in query.run(as_dict=True)}
@frappe.whitelist() @frappe.whitelist()

View File

@@ -931,6 +931,35 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, []) self.assertEqual(exc_je_for_pe, [])
@change_settings("Accounts Settings", {"add_taxes_from_item_tax_template": 1})
def test_18_fetch_taxes_based_on_taxes_and_charges_template(self):
# Create a Sales Taxes and Charges Template
if not frappe.db.exists("Sales Taxes and Charges Template", "_Test Tax - _TC"):
doc = frappe.new_doc("Sales Taxes and Charges Template")
doc.company = self.company
doc.title = "_Test Tax"
doc.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "Sales Expenses - _TC",
"description": "Test taxes",
"rate": 9,
},
)
doc.insert()
# Create a Sales Invoice
sinv = frappe.new_doc("Sales Invoice")
sinv.customer = self.customer
sinv.company = self.company
sinv.currency = "INR"
sinv.taxes_and_charges = "_Test Tax - _TC"
sinv.append("items", {"item_code": "_Test Item", "qty": 1, "rate": 50})
sinv.insert()
self.assertEqual(sinv.total_taxes_and_charges, 4.5)
def test_20_journal_against_sales_invoice(self): def test_20_journal_against_sales_invoice(self):
# Invoice in Foreign Currency # Invoice in Foreign Currency
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)

View File

@@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd."
app_description = """ERP made simple""" app_description = """ERP made simple"""
app_icon = "fa fa-th" app_icon = "fa fa-th"
app_color = "#e74c3c" app_color = "#e74c3c"
app_email = "info@erpnext.com" app_email = "hello@frappe.io"
app_license = "GNU General Public License (v3)" app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext" source_link = "https://github.com/frappe/erpnext"
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg" app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
@@ -484,7 +484,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
default_mail_footer = """ default_mail_footer = """
<span> <span>
Sent via Sent via
<a class="text-muted" href="https://erpnext.com?source=via_email_footer" target="_blank"> <a class="text-muted" href="https://frappe.io/erpnext?source=via_email_footer" target="_blank">
ERPNext ERPNext
</a> </a>
</span> </span>

View File

@@ -1371,7 +1371,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
}, },
) )
def get_max_op_qty(): def get_max_operation_quantity():
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Job Card") table = frappe.qb.DocType("Job Card")
@@ -1387,7 +1387,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
) )
return min([d.qty for d in query.run(as_dict=True)], default=0) return min([d.qty for d in query.run(as_dict=True)], default=0)
def get_utilised_cc(): def get_utilised_corrective_cost():
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Stock Entry") table = frappe.qb.DocType("Stock Entry")
@@ -1417,15 +1417,15 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None):
) )
) )
): ):
max_qty = get_max_op_qty() - work_order.produced_qty max_qty = get_max_operation_quantity() - work_order.produced_qty
remaining_cc = work_order.corrective_operation_cost - get_utilised_cc() remaining_corrective_cost = work_order.corrective_operation_cost - get_utilised_corrective_cost()
stock_entry.append( stock_entry.append(
"additional_costs", "additional_costs",
{ {
"expense_account": expense_account, "expense_account": expense_account,
"description": "Corrective Operation Cost", "description": "Corrective Operation Cost",
"has_corrective_cost": 1, "has_corrective_cost": 1,
"amount": remaining_cc / max_qty * flt(stock_entry.fg_completed_qty), "amount": remaining_corrective_cost / max_qty * flt(stock_entry.fg_completed_qty),
}, },
) )

View File

@@ -28,6 +28,8 @@ BOM_ITEM_FIELDS = [
"stock_uom", "stock_uom",
"conversion_factor", "conversion_factor",
"do_not_explode", "do_not_explode",
"source_warehouse",
"allow_alternative_item",
] ]
@@ -291,7 +293,6 @@ class BOMCreator(Document):
"item": row.item_code, "item": row.item_code,
"bom_type": "Production", "bom_type": "Production",
"quantity": row.qty, "quantity": row.qty,
"allow_alternative_item": 1,
"bom_creator": self.name, "bom_creator": self.name,
"bom_creator_item": bom_creator_item, "bom_creator_item": bom_creator_item,
} }
@@ -315,7 +316,6 @@ class BOMCreator(Document):
item_args.update( item_args.update(
{ {
"bom_no": bom_no, "bom_no": bom_no,
"allow_alternative_item": 1,
"allow_scrap_items": 1, "allow_scrap_items": 1,
"include_item_in_manufacturing": 1, "include_item_in_manufacturing": 1,
} }
@@ -428,6 +428,7 @@ def add_sub_assembly(**kwargs):
"do_not_explode": 1, "do_not_explode": 1,
"is_expandable": 1, "is_expandable": 1,
"stock_uom": item_info.stock_uom, "stock_uom": item_info.stock_uom,
"allow_alternative_item": kwargs.allow_alternative_item,
}, },
) )

View File

@@ -15,6 +15,7 @@
"is_expandable", "is_expandable",
"sourced_by_supplier", "sourced_by_supplier",
"bom_created", "bom_created",
"allow_alternative_item",
"description_section", "description_section",
"description", "description",
"quantity_and_rate_section", "quantity_and_rate_section",
@@ -225,12 +226,18 @@
"label": "BOM Created", "label": "BOM Created",
"no_copy": 1, "no_copy": 1,
"print_hide": 1 "print_hide": 1
},
{
"default": "1",
"fieldname": "allow_alternative_item",
"fieldtype": "Check",
"label": "Allow Alternative Item"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-06-03 18:45:24.339532", "modified": "2025-02-19 13:25:15.732496",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Creator Item", "name": "BOM Creator Item",

View File

@@ -14,6 +14,7 @@ class BOMCreatorItem(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
allow_alternative_item: DF.Check
amount: DF.Currency amount: DF.Currency
base_amount: DF.Currency base_amount: DF.Currency
base_rate: DF.Currency base_rate: DF.Currency

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.utils import getdate from frappe.utils import getdate, today
from erpnext.stock.report.stock_analytics.stock_analytics import get_period, get_period_date_ranges from erpnext.stock.report.stock_analytics.stock_analytics import get_period, get_period_date_ranges
@@ -30,11 +30,12 @@ def get_columns(filters):
def get_periodic_data(filters, entry): def get_periodic_data(filters, entry):
periodic_data = { periodic_data = {
"All Work Orders": {},
"Not Started": {}, "Not Started": {},
"Overdue": {}, "Overdue": {},
"Pending": {}, "Pending": {},
"Completed": {}, "Completed": {},
"Closed": {},
"Stopped": {},
} }
ranges = get_period_date_ranges(filters) ranges = get_period_date_ranges(filters)
@@ -42,33 +43,24 @@ def get_periodic_data(filters, entry):
for from_date, end_date in ranges: for from_date, end_date in ranges:
period = get_period(end_date, filters) period = get_period(end_date, filters)
for d in entry: for d in entry:
if getdate(d.creation) <= getdate(from_date) or getdate(d.creation) <= getdate(end_date): if getdate(from_date) <= getdate(d.creation) <= getdate(end_date) and d.status not in [
periodic_data = update_periodic_data(periodic_data, "All Work Orders", period) "Draft",
if d.status == "Completed": "Submitted",
if getdate(d.actual_end_date) < getdate(from_date) or getdate(d.modified) < getdate( "Completed",
from_date "Cancelled",
): ]:
periodic_data = update_periodic_data(periodic_data, "Completed", period) if d.status in ["Not Started", "Closed", "Stopped"]:
elif getdate(d.actual_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, d.status, period)
periodic_data = update_periodic_data(periodic_data, "Pending", period) elif today() > getdate(d.planned_end_date):
elif getdate(d.planned_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Overdue", period)
periodic_data = update_periodic_data(periodic_data, "Overdue", period) elif today() < getdate(d.planned_end_date):
else: periodic_data = update_periodic_data(periodic_data, "Pending", period)
periodic_data = update_periodic_data(periodic_data, "Not Started", period)
elif d.status == "In Process": if (
if getdate(d.actual_start_date) < getdate(from_date): getdate(from_date) <= getdate(d.actual_end_date) <= getdate(end_date)
periodic_data = update_periodic_data(periodic_data, "Pending", period) and d.status == "Completed"
elif getdate(d.planned_start_date) < getdate(from_date): ):
periodic_data = update_periodic_data(periodic_data, "Overdue", period) periodic_data = update_periodic_data(periodic_data, "Completed", period)
else:
periodic_data = update_periodic_data(periodic_data, "Not Started", period)
elif d.status == "Not Started":
if getdate(d.planned_start_date) < getdate(from_date):
periodic_data = update_periodic_data(periodic_data, "Overdue", period)
else:
periodic_data = update_periodic_data(periodic_data, "Not Started", period)
return periodic_data return periodic_data
@@ -88,10 +80,7 @@ def get_data(filters, columns):
"Work Order", "Work Order",
fields=[ fields=[
"creation", "creation",
"modified",
"actual_start_date",
"actual_end_date", "actual_end_date",
"planned_start_date",
"planned_end_date", "planned_end_date",
"status", "status",
], ],
@@ -100,7 +89,7 @@ def get_data(filters, columns):
periodic_data = get_periodic_data(filters, entry) periodic_data = get_periodic_data(filters, entry)
labels = ["All Work Orders", "Not Started", "Overdue", "Pending", "Completed"] labels = ["Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"]
chart_data = get_chart_data(periodic_data, columns) chart_data = get_chart_data(periodic_data, columns)
ranges = get_period_date_ranges(filters) ranges = get_period_date_ranges(filters)
@@ -121,21 +110,23 @@ def get_data(filters, columns):
def get_chart_data(periodic_data, columns): def get_chart_data(periodic_data, columns):
labels = [d.get("label") for d in columns[1:]] labels = [d.get("label") for d in columns[1:]]
all_data, not_start, overdue, pending, completed = [], [], [], [], [] not_start, overdue, pending, completed, closed, stopped = [], [], [], [], [], []
datasets = [] datasets = []
for d in labels: for d in labels:
all_data.append(periodic_data.get("All Work Orders").get(d))
not_start.append(periodic_data.get("Not Started").get(d)) not_start.append(periodic_data.get("Not Started").get(d))
overdue.append(periodic_data.get("Overdue").get(d)) overdue.append(periodic_data.get("Overdue").get(d))
pending.append(periodic_data.get("Pending").get(d)) pending.append(periodic_data.get("Pending").get(d))
completed.append(periodic_data.get("Completed").get(d)) completed.append(periodic_data.get("Completed").get(d))
closed.append(periodic_data.get("Closed").get(d))
stopped.append(periodic_data.get("Stopped").get(d))
datasets.append({"name": _("All Work Orders"), "values": all_data})
datasets.append({"name": _("Not Started"), "values": not_start}) datasets.append({"name": _("Not Started"), "values": not_start})
datasets.append({"name": _("Overdue"), "values": overdue}) datasets.append({"name": _("Overdue"), "values": overdue})
datasets.append({"name": _("Pending"), "values": pending}) datasets.append({"name": _("Pending"), "values": pending})
datasets.append({"name": _("Completed"), "values": completed}) datasets.append({"name": _("Completed"), "values": completed})
datasets.append({"name": _("Closed"), "values": closed})
datasets.append({"name": _("Stopped"), "values": stopped})
chart = {"data": {"labels": labels, "datasets": datasets}} chart = {"data": {"labels": labels, "datasets": datasets}}
chart["type"] = "line" chart["type"] = "line"

View File

@@ -261,6 +261,7 @@ erpnext.patches.v14_0.show_loan_management_deprecation_warning
erpnext.patches.v14_0.clear_reconciliation_values_from_singles erpnext.patches.v14_0.clear_reconciliation_values_from_singles
execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True) execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
erpnext.patches.v14_0.update_proprietorship_to_individual erpnext.patches.v14_0.update_proprietorship_to_individual
erpnext.patches.v15_0.rename_subcontracting_fields
[post_model_sync] [post_model_sync]
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
@@ -394,3 +395,9 @@ execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_post
erpnext.patches.v14_0.disable_add_row_in_gross_profit erpnext.patches.v14_0.disable_add_row_in_gross_profit
erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
erpnext.patches.v14_0.update_posting_datetime erpnext.patches.v14_0.update_posting_datetime
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
erpnext.patches.v15_0.rename_sla_fields
erpnext.patches.v15_0.update_query_report
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
erpnext.patches.v15_0.recalculate_amount_difference_field
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item

View File

@@ -82,6 +82,9 @@ def get_asset_depreciation_schedules_map():
.orderby(ds.idx) .orderby(ds.idx)
).run(as_dict=True) ).run(as_dict=True)
if len(records) > 20000:
frappe.db.auto_commit_on_many_writes = True
asset_depreciation_schedules_map = frappe._dict() asset_depreciation_schedules_map = frappe._dict()
for d in records: for d in records:
asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d) asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d)

View File

@@ -0,0 +1,80 @@
import frappe
from frappe.query_builder.functions import Sum
from frappe.utils import flt
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import adjust_incoming_rate_for_pr
def execute():
table = frappe.qb.DocType("Purchase Receipt Item")
parent = frappe.qb.DocType("Purchase Receipt")
query = (
frappe.qb.from_(table)
.join(parent)
.on(table.parent == parent.name)
.select(
table.parent,
table.name,
table.amount,
table.billed_amt,
table.amount_difference_with_purchase_invoice,
table.rate,
table.qty,
parent.conversion_rate,
)
.where((table.amount_difference_with_purchase_invoice != 0) & (table.docstatus == 1))
)
try:
if fiscal_year_dates := get_fiscal_year(frappe.utils.datetime.date.today()):
query.where(parent.posting_date.between(fiscal_year_dates[1], fiscal_year_dates[2]))
except Exception:
return
if result := query.run(as_dict=True):
item_wise_billed_qty = get_billed_qty_against_purchase_receipt([item.name for item in result])
purchase_receipts = set()
precision = frappe.get_precision("Purchase Receipt Item", "amount")
for item in result:
adjusted_amt = 0.0
if (
item.billed_amt is not None
and item.amount is not None
and item_wise_billed_qty.get(item.name)
):
adjusted_amt = (
flt(item.billed_amt / item_wise_billed_qty.get(item.name)) - flt(item.rate)
) * item.qty
adjusted_amt = flt(
adjusted_amt * flt(item.conversion_rate),
precision,
)
if adjusted_amt != item.amount_difference_with_purchase_invoice:
frappe.db.set_value(
"Purchase Receipt Item",
item.name,
"amount_difference_with_purchase_invoice",
adjusted_amt,
update_modified=False,
)
purchase_receipts.add(item.parent)
for pr in purchase_receipts:
adjust_incoming_rate_for_pr(frappe.get_doc("Purchase Receipt", pr))
def get_billed_qty_against_purchase_receipt(pr_names):
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(table.pr_detail, Sum(table.qty).as_("qty"))
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
)
invoice_data = query.run(as_list=1)
if not invoice_data:
return frappe._dict()
return frappe._dict(invoice_data)

View File

@@ -0,0 +1,17 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
frappe.db.set_value(
"DocField",
{"parent": "Purchase Receipt Item", "fieldname": "rate_difference_with_purchase_invoice"},
"label",
"Amount Difference with Purchase Invoice",
)
rename_field(
"Purchase Receipt Item",
"rate_difference_with_purchase_invoice",
"amount_difference_with_purchase_invoice",
)
frappe.clear_cache(doctype="Purchase Receipt Item")

View File

@@ -0,0 +1,13 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import rename_fieldname
from frappe.model.utils.rename_field import rename_field
def execute():
doctypes = frappe.get_all("Service Level Agreement", pluck="document_type")
for doctype in doctypes:
rename_fieldname(doctype + "-resolution_by", "sla_resolution_by")
rename_fieldname(doctype + "-resolution_date", "sla_resolution_date")
rename_field("Issue", "resolution_by", "sla_resolution_by")
rename_field("Issue", "resolution_date", "sla_resolution_date")

View File

@@ -0,0 +1,7 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
rename_field("Purchase Order Item", "sco_qty", "subcontracted_quantity")
rename_field("Subcontracting Order Item", "sc_conversion_factor", "subcontracting_conversion_factor")

View File

@@ -0,0 +1,21 @@
import frappe
def execute():
# nosemgrep
frappe.db.sql(
"""
UPDATE `tabAsset Capitalization Stock Item` ACSI
JOIN `tabAsset Capitalization` AC
ON ACSI.parent = AC.name
JOIN `tabPurchase Receipt Item` PRI
ON
PRI.item_code = ACSI.item_code
AND PRI.wip_composite_asset = AC.target_asset
SET
ACSI.purchase_receipt_item = PRI.name
WHERE
ACSI.purchase_receipt_item IS NULL
AND AC.docstatus = 1
"""
)

View File

@@ -0,0 +1,25 @@
import frappe
def execute():
reports = [
"Accounts Payable",
"Accounts Payable Summary",
"Purchase Register",
"Item-wise Purchase Register",
"Purchase Order Analysis",
"Received Items To Be Billed",
"Supplier Ledger Summary",
]
frappe.db.set_value(
"Workspace Link",
{
"parent": "Payables",
"link_type": "Report",
"type": "Link",
"link_to": ["in", reports],
"is_query_report": 0,
},
"is_query_report",
1,
)

View File

@@ -322,17 +322,31 @@ class Project(Document):
self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0 self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0
def update_billed_amount(self): def update_billed_amount(self):
# nosemgrep self.total_billed_amount = self.get_billed_amount_from_parent() + self.get_billed_amount_from_child()
def get_billed_amount_from_parent(self):
total_billed_amount = frappe.db.sql( total_billed_amount = frappe.db.sql(
"""select sum(base_net_amount) """select sum(base_net_amount)
from `tabSales Invoice Item` si_item, `tabSales Invoice` si from `tabSales Invoice` si join `tabSales Invoice Item` si_item on si_item.parent = si.name
where si_item.parent = si.name where si_item.project is null
and if(si_item.project, si_item.project, si.project) = %s and si.project is not null
and si.docstatus=1""", and si.project = %s
and si.docstatus = 1""",
self.name, self.name,
) )
self.total_billed_amount = total_billed_amount and total_billed_amount[0][0] or 0 return total_billed_amount and total_billed_amount[0][0] or 0
def get_billed_amount_from_child(self):
total_billed_amount = frappe.db.sql(
"""select sum(base_net_amount)
from `tabSales Invoice Item`
where project = %s
and docstatus = 1""",
self.name,
)
return total_billed_amount and total_billed_amount[0][0] or 0
def after_rename(self, old_name, new_name, merge=False): def after_rename(self, old_name, new_name, merge=False):
if old_name == self.copied_from: if old_name == self.copied_from:

View File

@@ -210,6 +210,13 @@ class BOMConfigurator {
[ [
{ label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 }, { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 },
{ label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 }, { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 },
{
label: __("Allow Alternative Item"),
fieldname: "allow_alternative_item",
default: 1.0,
fieldtype: "Check",
reqd: 1,
},
], ],
(data) => { (data) => {
if (!node.data.parent_id) { if (!node.data.parent_id) {
@@ -224,6 +231,7 @@ class BOMConfigurator {
item_code: data.item_code, item_code: data.item_code,
fg_reference_id: node.data.name || this.frm.doc.name, fg_reference_id: node.data.name || this.frm.doc.name,
qty: data.qty, qty: data.qty,
allow_alternative_item: data.allow_alternative_item,
}, },
callback: (r) => { callback: (r) => {
view.events.load_tree(r, node); view.events.load_tree(r, node);
@@ -258,6 +266,7 @@ class BOMConfigurator {
fg_item: node.data.value, fg_item: node.data.value,
fg_reference_id: node.data.name || this.frm.doc.name, fg_reference_id: node.data.name || this.frm.doc.name,
bom_item: bom_item, bom_item: bom_item,
allow_alternative_item: bom_item.allow_alternative_item,
}, },
callback: (r) => { callback: (r) => {
view.events.load_tree(r, node); view.events.load_tree(r, node);
@@ -278,6 +287,14 @@ class BOMConfigurator {
reqd: 1, reqd: 1,
read_only: read_only, read_only: read_only,
}, },
{
label: __("Allow Alternative Item"),
fieldname: "allow_alternative_item",
default: 1.0,
fieldtype: "Check",
reqd: 1,
read_only: read_only,
},
{ fieldtype: "Column Break" }, { fieldtype: "Column Break" },
{ {
label: __("Qty"), label: __("Qty"),

View File

@@ -1857,7 +1857,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
const exist_items = items.map(row => { return {item_code: row.item_code, pricing_rules: row.pricing_rules};}); const exist_items = items.map(row => { return {item_code: row.item_code, pricing_rules: row.pricing_rules};});
args.free_item_data.forEach(pr_row => { args.free_item_data.forEach(async pr_row => {
let row_to_modify = {}; let row_to_modify = {};
// If there are no free items, or if the current free item doesn't exist in the table, add it // If there are no free items, or if the current free item doesn't exist in the table, add it
@@ -1875,6 +1875,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
for (let key in pr_row) { for (let key in pr_row) {
row_to_modify[key] = pr_row[key]; row_to_modify[key] = pr_row[key];
} }
if (this.frm.doc.hasOwnProperty("is_pos") && this.frm.doc.is_pos) {
let r = await frappe.db.get_value("POS Profile", this.frm.doc.pos_profile, "cost_center");
if (r.message.cost_center) {
row_to_modify["cost_center"] = r.message.cost_center;
}
}
this.frm.script_manager.copy_from_first_row("items", row_to_modify, ["expense_account", "income_account"]); this.frm.script_manager.copy_from_first_row("items", row_to_modify, ["expense_account", "income_account"]);
}); });

View File

@@ -14,6 +14,10 @@ erpnext.landed_cost_taxes_and_charges = {
"Income Account", "Income Account",
"Expenses Included In Valuation", "Expenses Included In Valuation",
"Expenses Included In Asset Valuation", "Expenses Included In Asset Valuation",
"Expense Account",
"Direct Expense",
"Indirect Expense",
"Stock Received But Not Billed",
], ],
], ],
company: frm.doc.company, company: frm.doc.company,

View File

@@ -540,6 +540,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
has_batch_no: this.item.has_batch_no, has_batch_no: this.item.has_batch_no,
qty: qty, qty: qty,
based_on: based_on, based_on: based_on,
posting_date: this.frm.doc.posting_date,
posting_time: this.frm.doc.posting_time,
}, },
callback: (r) => { callback: (r) => {
if (r.message) { if (r.message) {

View File

@@ -1,4 +1,4 @@
{{ address_line1 }}<br> {{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%} {% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}<br>{% endif -%} {{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}<br>
{% if country != "United States" %}{{ country }}{% endif -%} {% if country != "United States" %}{{ country }}{% endif -%}

View File

@@ -188,9 +188,9 @@
<Descrizione>{{ html2text(item.description or '') or item.item_name }}</Descrizione> <Descrizione>{{ html2text(item.description or '') or item.item_name }}</Descrizione>
<Quantita>{{ format_float(item.qty) }}</Quantita> <Quantita>{{ format_float(item.qty) }}</Quantita>
<UnitaMisura>{{ item.stock_uom }}</UnitaMisura> <UnitaMisura>{{ item.stock_uom }}</UnitaMisura>
<PrezzoUnitario>{{ format_float(item.price_list_rate or item.rate, item_meta.get_field("rate").precision) }}</PrezzoUnitario> <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) }} {{ render_discount_or_margin(item) }}
<PrezzoTotale>{{ format_float(item.amount, item_meta.get_field("amount").precision) }}</PrezzoTotale> <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> <AliquotaIVA>{{ format_float(item.tax_rate, item_meta.get_field("tax_rate").precision) }}</AliquotaIVA>
{%- if item.tax_exemption_reason %} {%- if item.tax_exemption_reason %}
<Natura>{{ item.tax_exemption_reason.split("-")[0] }}</Natura> <Natura>{{ item.tax_exemption_reason.split("-")[0] }}</Natura>

View File

@@ -1044,7 +1044,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
ignore_permissions=True, ignore_permissions=True,
) )
dn_item.qty = flt(sre.reserved_qty) * flt(dn_item.get("conversion_factor", 1)) dn_item.qty = flt(sre.reserved_qty) / flt(dn_item.get("conversion_factor", 1))
if sre.reservation_based_on == "Serial and Batch" and (sre.has_serial_no or sre.has_batch_no): if sre.reservation_based_on == "Serial and Batch" and (sre.has_serial_no or sre.has_batch_no):
dn_item.serial_and_batch_bundle = get_ssb_bundle_for_voucher(sre) dn_item.serial_and_batch_bundle = get_ssb_bundle_for_voucher(sre)

View File

@@ -2097,6 +2097,45 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0) frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
def test_delivery_note_rate_on_change_of_warehouse(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
item = make_item(
"_Test Batch Item for Delivery Note Rate",
{
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BH-SDDTBIFRM-.#####",
},
)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
so = make_sales_order(
item_code=item.name, rate=27648.00, price_list_rate=27648.00, qty=1, do_not_submit=True
)
so.items[0].rate = 90
so.save()
self.assertTrue(so.items[0].discount_amount == 27558.0)
so.submit()
warehouse = create_warehouse("NW Warehouse FOR Rate", company=so.company)
make_stock_entry(
item_code=item.name,
qty=2,
target=warehouse,
basic_rate=100,
company=so.company,
use_serial_batch_fields=1,
)
dn = make_delivery_note(so.name)
dn.items[0].warehouse = warehouse
dn.save()
self.assertEqual(dn.items[0].rate, 90)
def test_credit_limit_on_so_reopning(self): def test_credit_limit_on_so_reopning(self):
# set credit limit # set credit limit
company = "_Test Company" company = "_Test Company"

View File

@@ -115,6 +115,14 @@ def filter_result_items(result, pos_profile):
result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups] result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups]
@frappe.whitelist()
def get_parent_item_group():
# Using get_all to ignore user permission
item_group = frappe.get_all("Item Group", {"lft": 1, "is_group": 1}, pluck="name")
if item_group:
return item_group[0]
@frappe.whitelist() @frappe.whitelist()
def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""): def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""):
warehouse, hide_unavailable_items = frappe.db.get_value( warehouse, hide_unavailable_items = frappe.db.get_value(
@@ -320,13 +328,13 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = [] invoice_list = []
if search_term and status: if search_term and status:
invoices_by_customer = frappe.db.get_all( invoices_by_customer = frappe.db.get_list(
"POS Invoice", "POS Invoice",
filters={"customer": ["like", f"%{search_term}%"], "status": status}, filters={"customer": ["like", f"%{search_term}%"], "status": status},
fields=fields, fields=fields,
page_length=limit, page_length=limit,
) )
invoices_by_name = frappe.db.get_all( invoices_by_name = frappe.db.get_list(
"POS Invoice", "POS Invoice",
filters={"name": ["like", f"%{search_term}%"], "status": status}, filters={"name": ["like", f"%{search_term}%"], "status": status},
fields=fields, fields=fields,
@@ -335,7 +343,7 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = invoices_by_customer + invoices_by_name invoice_list = invoices_by_customer + invoices_by_name
elif status: elif status:
invoice_list = frappe.db.get_all( invoice_list = frappe.db.get_list(
"POS Invoice", filters={"status": status}, fields=fields, page_length=limit "POS Invoice", filters={"status": status}, fields=fields, page_length=limit
) )

View File

@@ -605,6 +605,14 @@ erpnext.PointOfSale.Controller = class {
if (this.is_current_item_being_edited(item_row) || from_selector) { if (this.is_current_item_being_edited(item_row) || from_selector) {
await frappe.model.set_value(item_row.doctype, item_row.name, field, value); await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
if (item.serial_no && from_selector) {
await frappe.model.set_value(
item_row.doctype,
item_row.name,
"serial_no",
item_row.serial_no + `\n${item.serial_no}`
);
}
this.update_cart_html(item_row); this.update_cart_html(item_row);
} }
} else { } else {

View File

@@ -187,6 +187,7 @@ erpnext.PointOfSale.ItemDetails = class {
this[`${fieldname}_control`].set_value(item[fieldname]); this[`${fieldname}_control`].set_value(item[fieldname]);
}); });
this.resize_serial_control(item);
this.make_auto_serial_selection_btn(item); this.make_auto_serial_selection_btn(item);
this.bind_custom_control_change_event(); this.bind_custom_control_change_event();
@@ -203,28 +204,27 @@ erpnext.PointOfSale.ItemDetails = class {
"actual_qty", "actual_qty",
"price_list_rate", "price_list_rate",
]; ];
if (item.has_serial_no) fields.push("serial_no"); if (item.has_serial_no || item.serial_no) fields.push("serial_no");
if (item.has_batch_no) fields.push("batch_no"); if (item.has_batch_no || item.batch_no) fields.push("batch_no");
return fields; return fields;
} }
resize_serial_control(item) {
if (item.has_serial_no || item.serial_no) {
this.$form_container.find(".serial_no-control").find("textarea").css("height", "6rem");
}
}
make_auto_serial_selection_btn(item) { make_auto_serial_selection_btn(item) {
if (item.has_serial_no || item.has_batch_no) { const doc = this.events.get_frm().doc;
if (item.has_serial_no && item.has_batch_no) { if (!doc.is_return && (item.has_serial_no || item.serial_no)) {
this.$form_container.append( if (!item.has_batch_no) {
`<div class="btn btn-sm btn-secondary auto-fetch-btn" style="margin-top: 6px">${__( this.$form_container.append(`<div class="grid-filler no-select"></div>`);
"Select Serial No / Batch No"
)}</div>`
);
} else {
const classname = item.has_serial_no ? ".serial_no-control" : ".batch_no-control";
const label = item.has_serial_no ? __("Select Serial No") : __("Select Batch No");
this.$form_container
.find(classname)
.append(
`<div class="btn btn-sm btn-secondary auto-fetch-btn" style="margin-top: 6px">${label}</div>`
);
} }
const label = __("Auto Fetch Serial Numbers");
this.$form_container.append(
`<div class="btn btn-sm btn-secondary auto-fetch-btn">${label}</div>`
);
this.$form_container.find(".serial_no-control").find("textarea").css("height", "6rem"); this.$form_container.find(".serial_no-control").find("textarea").css("height", "6rem");
} }
} }
@@ -410,18 +410,41 @@ erpnext.PointOfSale.ItemDetails = class {
bind_auto_serial_fetch_event() { bind_auto_serial_fetch_event() {
this.$form_container.on("click", ".auto-fetch-btn", () => { this.$form_container.on("click", ".auto-fetch-btn", () => {
let frm = this.events.get_frm(); this.batch_no_control && this.batch_no_control.set_value("");
let item_row = this.item_row; let qty = this.qty_control.get_value();
item_row.type_of_transaction = "Outward"; let conversion_factor = this.conversion_factor_control.get_value();
let expiry_date = this.item_row.has_batch_no ? this.events.get_frm().doc.posting_date : "";
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { let numbers = frappe.call({
if (r) { method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
frappe.model.set_value(item_row.doctype, item_row.name, { args: {
serial_and_batch_bundle: r.name, qty: qty * conversion_factor,
qty: Math.abs(r.total_qty), item_code: this.current_item.item_code,
use_serial_batch_fields: 0, warehouse: this.warehouse_control.get_value() || "",
}); batch_nos: this.current_item.batch_no || "",
posting_date: expiry_date,
for_doctype: "POS Invoice",
},
});
numbers.then((data) => {
let auto_fetched_serial_numbers = data.message;
let records_length = auto_fetched_serial_numbers.length;
if (!records_length) {
const warehouse = this.warehouse_control.get_value().bold();
const item_code = this.current_item.item_code.bold();
frappe.msgprint(
__(
"Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.",
[item_code, warehouse]
)
);
} else if (records_length < qty) {
frappe.msgprint(__("Fetched only {0} available serial numbers.", [records_length]));
this.qty_control.set_value(records_length);
} }
numbers = auto_fetched_serial_numbers.join(`\n`);
this.serial_no_control.set_value(numbers);
}); });
}); });
} }

View File

@@ -38,8 +38,13 @@ erpnext.PointOfSale.ItemSelector = class {
async load_items_data() { async load_items_data() {
if (!this.item_group) { if (!this.item_group) {
const res = await frappe.db.get_value("Item Group", { lft: 1, is_group: 1 }, "name"); frappe.call({
this.parent_item_group = res.message.name; method: "erpnext.selling.page.point_of_sale.point_of_sale.get_parent_item_group",
async: false,
callback: (r) => {
if (r.message) this.parent_item_group = r.message;
},
});
} }
if (!this.price_list) { if (!this.price_list) {
const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list");

View File

@@ -340,19 +340,11 @@ erpnext.PointOfSale.Payment = class {
// pass // pass
} }
async render_payment_section() { render_payment_section() {
this.render_payment_mode_dom(); this.render_payment_mode_dom();
this.make_invoice_fields_control(); this.make_invoice_fields_control();
this.update_totals_section(); this.update_totals_section();
let r = await frappe.db.get_value( this.unset_grand_total_to_default_mop();
"POS Profile",
this.frm.doc.pos_profile,
"disable_grand_total_to_default_mop"
);
if (!r.message.disable_grand_total_to_default_mop) {
this.focus_on_default_mop();
}
} }
after_render() { after_render() {
@@ -637,6 +629,19 @@ erpnext.PointOfSale.Payment = class {
.toLowerCase(); .toLowerCase();
} }
async unset_grand_total_to_default_mop() {
const doc = this.events.get_frm().doc;
let r = await frappe.db.get_value(
"POS Profile",
doc.pos_profile,
"disable_grand_total_to_default_mop"
);
if (!r.message.disable_grand_total_to_default_mop) {
this.focus_on_default_mop();
}
}
validate_reqd_invoice_fields() { validate_reqd_invoice_fields() {
const doc = this.events.get_frm().doc; const doc = this.events.get_frm().doc;
let validation_flag = true; let validation_flag = true;

View File

@@ -19,6 +19,10 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80", width: "80",
reqd: 1, reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
on_change: (report) => {
report.set_filter_value("sales_order", []);
report.refresh();
},
}, },
{ {
fieldname: "to_date", fieldname: "to_date",
@@ -27,6 +31,10 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80", width: "80",
reqd: 1, reqd: 1,
default: frappe.datetime.get_today(), default: frappe.datetime.get_today(),
on_change: (report) => {
report.set_filter_value("sales_order", []);
report.refresh();
},
}, },
{ {
fieldname: "sales_order", fieldname: "sales_order",
@@ -35,12 +43,13 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80", width: "80",
options: "Sales Order", options: "Sales Order",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Sales Order", txt); let filters = { docstatus: 1 };
},
get_query: () => { const from_date = frappe.query_report.get_filter_value("from_date");
return { const to_date = frappe.query_report.get_filter_value("to_date");
filters: { docstatus: 1 }, if (from_date && to_date) filters["transaction_date"] = ["between", [from_date, to_date]];
};
return frappe.db.get_link_options("Sales Order", txt, filters);
}, },
}, },
{ {
@@ -53,10 +62,17 @@ frappe.query_reports["Sales Order Analysis"] = {
fieldname: "status", fieldname: "status",
label: __("Status"), label: __("Status"),
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"], options: ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed", "Closed"],
width: "80", width: "80",
get_data: function (txt) { get_data: function (txt) {
let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"]; let status = [
"To Pay",
"To Bill",
"To Deliver",
"To Deliver and Bill",
"Completed",
"Closed",
];
let options = []; let options = [];
for (let option of status) { for (let option of status) {
options.push({ options.push({

View File

@@ -86,7 +86,7 @@ def get_data(conditions, filters):
ON sii.so_detail = soi.name and sii.docstatus = 1 ON sii.so_detail = soi.name and sii.docstatus = 1
WHERE WHERE
soi.parent = so.name soi.parent = so.name
and so.status not in ('Stopped', 'Closed', 'On Hold') and so.status not in ('Stopped', 'On Hold')
and so.docstatus = 1 and so.docstatus = 1
{conditions} {conditions}
GROUP BY soi.name GROUP BY soi.name

View File

@@ -16,7 +16,7 @@ from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
from .default_success_action import get_default_success_action from .default_success_action import get_default_success_action
default_mail_footer = """<div style="padding: 7px; text-align: right; color: #888"><small>Sent via default_mail_footer = """<div style="padding: 7px; text-align: right; color: #888"><small>Sent via
<a style="color: #888" href="http://erpnext.org">ERPNext</a></div>""" <a style="color: #888" href="http://frappe.io/erpnext">ERPNext</a></div>"""
def after_install(): def after_install():

View File

@@ -68,9 +68,6 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
if not transaction_date: if not transaction_date:
transaction_date = nowdate() transaction_date = nowdate()
if rate := get_pegged_rate(from_currency, to_currency, transaction_date):
return rate
currency_settings = frappe.get_doc("Accounts Settings").as_dict() currency_settings = frappe.get_doc("Accounts Settings").as_dict()
allow_stale_rates = currency_settings.get("allow_stale") allow_stale_rates = currency_settings.get("allow_stale")
@@ -100,6 +97,9 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"): if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"):
return 0.00 return 0.00
if rate := get_pegged_rate(from_currency, to_currency, transaction_date):
return rate
try: try:
cache = frappe.cache() cache = frappe.cache()
key = f"currency_exchange_rate_{transaction_date}:{from_currency}:{to_currency}" key = f"currency_exchange_rate_{transaction_date}:{from_currency}:{to_currency}"

View File

@@ -2,6 +2,13 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Pick List", { frappe.ui.form.on("Pick List", {
after_save(frm) {
setTimeout(() => {
// Added to fix the issue of locations table not getting updated after save
frm.reload_doc();
}, 500);
},
setup: (frm) => { setup: (frm) => {
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];

View File

@@ -73,6 +73,7 @@ class PickList(Document):
self.set_onload("has_reserved_stock", True) self.set_onload("has_reserved_stock", True)
def validate(self): def validate(self):
self.validate_expired_batches()
self.validate_for_qty() self.validate_for_qty()
self.validate_stock_qty() self.validate_stock_qty()
self.check_serial_no_status() self.check_serial_no_status()
@@ -205,6 +206,33 @@ class PickList(Document):
self.update_reference_qty() self.update_reference_qty()
self.update_sales_order_picking_status() self.update_sales_order_picking_status()
def validate_expired_batches(self):
batches = []
for row in self.get("locations"):
if row.get("batch_no") and row.get("picked_qty"):
batches.append(row.batch_no)
if batches:
batch = frappe.qb.DocType("Batch")
query = (
frappe.qb.from_(batch)
.select(batch.name)
.where(
(batch.name.isin(batches))
& (batch.expiry_date <= frappe.utils.nowdate())
& (batch.expiry_date.isnotnull())
)
)
expired_batches = query.run(as_dict=True)
if expired_batches:
msg = "<ul>" + "".join(f"<li>{batch.name}</li>" for batch in expired_batches) + "</ul>"
frappe.throw(
_("The following batches are expired, please restock them: <br> {0}").format(msg),
title=_("Expired Batches"),
)
def make_bundle_using_old_serial_batch_fields(self): def make_bundle_using_old_serial_batch_fields(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -447,6 +475,7 @@ class PickList(Document):
self.remove(row) self.remove(row)
updated_locations = frappe._dict() updated_locations = frappe._dict()
len_idx = len(self.get("locations")) or 0
for item_doc in items: for item_doc in items:
item_code = item_doc.item_code item_code = item_doc.item_code
@@ -489,6 +518,8 @@ class PickList(Document):
if location.picked_qty > location.stock_qty: if location.picked_qty > location.stock_qty:
location.picked_qty = location.stock_qty location.picked_qty = location.stock_qty
len_idx += 1
location.idx = len_idx
self.append("locations", location) self.append("locations", location)
# If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red # If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red
@@ -497,7 +528,11 @@ class PickList(Document):
for location in locations_replica: for location in locations_replica:
location.stock_qty = 0 location.stock_qty = 0
location.picked_qty = 0 location.picked_qty = 0
len_idx += 1
location.idx = len_idx
self.append("locations", location) self.append("locations", location)
frappe.msgprint( frappe.msgprint(
_( _(
"Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List." "Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List."
@@ -638,8 +673,31 @@ class PickList(Document):
if serial_no: if serial_no:
picked_items[item_data.item_code][key]["serial_no"].extend(serial_no) picked_items[item_data.item_code][key]["serial_no"].extend(serial_no)
self.update_picked_item_from_current_pick_list(picked_items)
return picked_items return picked_items
def update_picked_item_from_current_pick_list(self, picked_items):
for row in self.locations:
if flt(row.picked_qty) > 0:
key = (row.warehouse, row.batch_no) if row.batch_no else row.warehouse
serial_no = [x for x in row.serial_no.split("\n") if x] if row.serial_no else None
if row.item_code not in picked_items:
picked_items[row.item_code] = {}
if key not in picked_items[row.item_code]:
picked_items[row.item_code][key] = frappe._dict(
{
"picked_qty": 0,
"serial_no": [],
"batch_no": row.batch_no or "",
"warehouse": row.warehouse,
}
)
picked_items[row.item_code][key]["picked_qty"] += flt(row.stock_qty) or flt(row.picked_qty)
if serial_no:
picked_items[row.item_code][key]["serial_no"].extend(serial_no)
def _get_pick_list_items(self, items): def _get_pick_list_items(self, items):
pi = frappe.qb.DocType("Pick List") pi = frappe.qb.DocType("Pick List")
pi_item = frappe.qb.DocType("Pick List Item") pi_item = frappe.qb.DocType("Pick List Item")
@@ -653,9 +711,11 @@ class PickList(Document):
pi_item.batch_no, pi_item.batch_no,
pi_item.serial_and_batch_bundle, pi_item.serial_and_batch_bundle,
pi_item.serial_no, pi_item.serial_no,
(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_( (
"picked_qty" Case()
), .when((pi_item.picked_qty > 0) & (pi_item.docstatus == 1), pi_item.picked_qty)
.else_(pi_item.stock_qty)
).as_("picked_qty"),
) )
.where( .where(
(pi_item.item_code.isin([x.item_code for x in items])) (pi_item.item_code.isin([x.item_code for x in items]))

View File

@@ -424,6 +424,14 @@ class PurchaseReceipt(BuyingController):
self.delete_auto_created_batches() self.delete_auto_created_batches()
self.set_consumed_qty_in_subcontract_order() self.set_consumed_qty_in_subcontract_order()
def before_cancel(self):
super().before_cancel()
self.remove_amount_difference_with_purchase_invoice()
def remove_amount_difference_with_purchase_invoice(self):
for item in self.items:
item.amount_difference_with_purchase_invoice = 0
def get_gl_entries(self, warehouse_account=None, via_landed_cost_voucher=False): def get_gl_entries(self, warehouse_account=None, via_landed_cost_voucher=False):
from erpnext.accounts.general_ledger import process_gl_map from erpnext.accounts.general_ledger import process_gl_map
@@ -571,15 +579,15 @@ class PurchaseReceipt(BuyingController):
item=item, item=item,
) )
def make_rate_difference_entry(item): def make_amount_difference_entry(item):
if item.rate_difference_with_purchase_invoice and stock_asset_rbnb: if item.amount_difference_with_purchase_invoice and stock_asset_rbnb:
account_currency = get_account_currency(stock_asset_rbnb) account_currency = get_account_currency(stock_asset_rbnb)
self.add_gl_entry( self.add_gl_entry(
gl_entries=gl_entries, gl_entries=gl_entries,
account=stock_asset_rbnb, account=stock_asset_rbnb,
cost_center=item.cost_center, cost_center=item.cost_center,
debit=0.0, debit=0.0,
credit=flt(item.rate_difference_with_purchase_invoice), credit=flt(item.amount_difference_with_purchase_invoice),
remarks=_("Adjustment based on Purchase Invoice rate"), remarks=_("Adjustment based on Purchase Invoice rate"),
against_account=stock_asset_account_name, against_account=stock_asset_account_name,
account_currency=account_currency, account_currency=account_currency,
@@ -612,7 +620,7 @@ class PurchaseReceipt(BuyingController):
+ flt(item.landed_cost_voucher_amount) + flt(item.landed_cost_voucher_amount)
+ flt(item.rm_supp_cost) + flt(item.rm_supp_cost)
+ flt(item.item_tax_amount) + flt(item.item_tax_amount)
+ flt(item.rate_difference_with_purchase_invoice) + flt(item.amount_difference_with_purchase_invoice)
) )
divisional_loss = flt( divisional_loss = flt(
@@ -712,7 +720,7 @@ class PurchaseReceipt(BuyingController):
make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name) make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
outgoing_amount = make_stock_received_but_not_billed_entry(d) outgoing_amount = make_stock_received_but_not_billed_entry(d)
make_landed_cost_gl_entries(d) make_landed_cost_gl_entries(d)
make_rate_difference_entry(d) make_amount_difference_entry(d)
make_sub_contracting_gl_entries(d) make_sub_contracting_gl_entries(d)
make_divisional_loss_gl_entry(d, outgoing_amount) make_divisional_loss_gl_entry(d, outgoing_amount)
elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or ( elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or (
@@ -1048,15 +1056,19 @@ def get_billed_amount_against_po(po_items):
if not po_items: if not po_items:
return {} return {}
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = ( query = (
frappe.qb.from_(purchase_invoice_item) frappe.qb.from_(purchase_invoice_item)
.inner_join(purchase_invoice)
.on(purchase_invoice_item.parent == purchase_invoice.name)
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail) .select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail)
.where( .where(
(purchase_invoice_item.po_detail.isin(po_items)) (purchase_invoice_item.po_detail.isin(po_items))
& (purchase_invoice_item.docstatus == 1) & (purchase_invoice.docstatus == 1)
& (purchase_invoice_item.pr_detail.isnull()) & (purchase_invoice_item.pr_detail.isnull())
& (purchase_invoice.update_stock == 0)
) )
.groupby(purchase_invoice_item.po_detail) .groupby(purchase_invoice_item.po_detail)
).run(as_dict=1) ).run(as_dict=1)
@@ -1090,11 +1102,19 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
if adjust_incoming_rate: if adjust_incoming_rate:
adjusted_amt = 0.0 adjusted_amt = 0.0
if item.billed_amt is not None and item.amount is not None: item_wise_billed_qty = get_billed_qty_against_purchase_receipt(pr_doc)
adjusted_amt = flt(item.billed_amt) - flt(item.amount)
adjusted_amt = adjusted_amt * flt(pr_doc.conversion_rate) if (
item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False) item.billed_amt is not None
and item.amount is not None
and item_wise_billed_qty.get(item.name)
):
adjusted_amt = (
flt(item.billed_amt / item_wise_billed_qty.get(item.name)) - flt(item.rate)
) * item.qty
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed) pr_doc.db_set("per_billed", percent_billed)
@@ -1107,6 +1127,21 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
adjust_incoming_rate_for_pr(pr_doc) adjust_incoming_rate_for_pr(pr_doc)
def get_billed_qty_against_purchase_receipt(pr_doc):
pr_names = [d.name for d in pr_doc.items]
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(table.pr_detail, fn.Sum(table.qty).as_("qty"))
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
)
invoice_data = query.run(as_list=1)
if not invoice_data:
return frappe._dict()
return frappe._dict(invoice_data)
def adjust_incoming_rate_for_pr(doc): def adjust_incoming_rate_for_pr(doc):
doc.update_valuation_rate(reset_outgoing_rate=False) doc.update_valuation_rate(reset_outgoing_rate=False)

View File

@@ -4047,6 +4047,36 @@ class TestPurchaseReceipt(FrappeTestCase):
batch_return.save() batch_return.save()
batch_return.submit() batch_return.submit()
def test_pr_status_based_on_invoices_with_update_stock(self):
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as _make_purchase_invoice,
)
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_receipt as _make_purchase_receipt,
)
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_pr_against_po,
create_purchase_order,
)
item_code = "Test Item for PR Status Based on Invoices"
create_item(item_code)
po = create_purchase_order(item_code=item_code, qty=10)
pi = _make_purchase_invoice(po.name)
pi.update_stock = 1
pi.items[0].qty = 5
pi.submit()
po.reload()
self.assertEqual(po.per_billed, 50)
pr = _make_purchase_receipt(po.name)
self.assertEqual(pr.items[0].qty, 5)
pr.submit()
pr.reload()
self.assertEqual(pr.status, "To Bill")
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -71,7 +71,7 @@
"item_tax_amount", "item_tax_amount",
"rm_supp_cost", "rm_supp_cost",
"landed_cost_voucher_amount", "landed_cost_voucher_amount",
"rate_difference_with_purchase_invoice", "amount_difference_with_purchase_invoice",
"billed_amt", "billed_amt",
"warehouse_and_reference", "warehouse_and_reference",
"warehouse", "warehouse",
@@ -998,14 +998,6 @@
"label": "Has Item Scanned", "label": "Has Item Scanned",
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "rate_difference_with_purchase_invoice",
"fieldtype": "Currency",
"label": "Rate Difference with Purchase Invoice",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{ {
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1", "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle", "fieldname": "serial_and_batch_bundle",
@@ -1135,12 +1127,20 @@
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "amount_difference_with_purchase_invoice",
"fieldtype": "Currency",
"label": "Amount Difference with Purchase Invoice",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-07-19 12:14:21.521466", "modified": "2025-02-17 13:15:36.692202",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

View File

@@ -16,6 +16,7 @@ class PurchaseReceiptItem(Document):
allow_zero_valuation_rate: DF.Check allow_zero_valuation_rate: DF.Check
amount: DF.Currency amount: DF.Currency
amount_difference_with_purchase_invoice: DF.Currency
apply_tds: DF.Check apply_tds: DF.Check
asset_category: DF.Link | None asset_category: DF.Link | None
asset_location: DF.Link | None asset_location: DF.Link | None
@@ -76,7 +77,6 @@ class PurchaseReceiptItem(Document):
qty: DF.Float qty: DF.Float
quality_inspection: DF.Link | None quality_inspection: DF.Link | None
rate: DF.Currency rate: DF.Currency
rate_difference_with_purchase_invoice: DF.Currency
rate_with_margin: DF.Currency rate_with_margin: DF.Currency
received_qty: DF.Float received_qty: DF.Float
received_stock_qty: DF.Float received_stock_qty: DF.Float

View File

@@ -230,15 +230,17 @@ def get_pos_reserved_serial_nos(filters):
pos_transacted_sr_nos = query.run(as_dict=True) pos_transacted_sr_nos = query.run(as_dict=True)
reserved_sr_nos = set() reserved_sr_nos = list()
returned_sr_nos = set() returned_sr_nos = list()
for d in pos_transacted_sr_nos: for d in pos_transacted_sr_nos:
if d.is_return == 0: if d.is_return == 0:
[reserved_sr_nos.add(x) for x in get_serial_nos(d.serial_no)] [reserved_sr_nos.append(x) for x in get_serial_nos(d.serial_no)]
elif d.is_return == 1: elif d.is_return == 1:
[returned_sr_nos.add(x) for x in get_serial_nos(d.serial_no)] [returned_sr_nos.append(x) for x in get_serial_nos(d.serial_no)]
reserved_sr_nos = list(reserved_sr_nos - returned_sr_nos) for x in returned_sr_nos:
if x in reserved_sr_nos:
reserved_sr_nos.remove(x)
return reserved_sr_nos return reserved_sr_nos
@@ -254,12 +256,7 @@ def fetch_serial_numbers(filters, qty, do_not_include=None):
query = ( query = (
frappe.qb.from_(serial_no) frappe.qb.from_(serial_no)
.select(serial_no.name) .select(serial_no.name)
.where( .where((serial_no.item_code == filters["item_code"]) & (serial_no.warehouse == filters["warehouse"]))
(serial_no.item_code == filters["item_code"])
& (serial_no.warehouse == filters["warehouse"])
& (Coalesce(serial_no.sales_invoice, "") == "")
& (Coalesce(serial_no.delivery_document_no, "") == "")
)
.orderby(serial_no.creation) .orderby(serial_no.creation)
.limit(qty or 1) .limit(qty or 1)
) )

View File

@@ -34,6 +34,7 @@
"shipment_parcel", "shipment_parcel",
"parcel_template", "parcel_template",
"add_template", "add_template",
"total_weight",
"column_break_28", "column_break_28",
"shipment_delivery_note", "shipment_delivery_note",
"shipment_details_section", "shipment_details_section",
@@ -429,11 +430,17 @@
"label": "Pickup Contact Person", "label": "Pickup Contact Person",
"mandatory_depends_on": "eval:doc.pickup_from_type === 'Company'", "mandatory_depends_on": "eval:doc.pickup_from_type === 'Company'",
"options": "User" "options": "User"
},
{
"fieldname": "total_weight",
"fieldtype": "Float",
"label": "Total Weight (kg)",
"read_only": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-17 17:23:27.025802", "modified": "2025-02-20 16:55:20.076418",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Shipment", "name": "Shipment",

View File

@@ -65,6 +65,7 @@ class Shipment(Document):
shipment_parcel: DF.Table[ShipmentParcel] shipment_parcel: DF.Table[ShipmentParcel]
shipment_type: DF.Literal["Goods", "Documents"] shipment_type: DF.Literal["Goods", "Documents"]
status: DF.Literal["Draft", "Submitted", "Booked", "Cancelled", "Completed"] status: DF.Literal["Draft", "Submitted", "Booked", "Cancelled", "Completed"]
total_weight: DF.Float
tracking_status: DF.Literal["", "In Progress", "Delivered", "Returned", "Lost"] tracking_status: DF.Literal["", "In Progress", "Delivered", "Returned", "Lost"]
tracking_status_info: DF.Data | None tracking_status_info: DF.Data | None
tracking_url: DF.SmallText | None tracking_url: DF.SmallText | None
@@ -75,6 +76,7 @@ class Shipment(Document):
self.validate_weight() self.validate_weight()
self.validate_pickup_time() self.validate_pickup_time()
self.set_value_of_goods() self.set_value_of_goods()
self.set_total_weight()
if self.docstatus == 0: if self.docstatus == 0:
self.status = "Draft" self.status = "Draft"
@@ -93,6 +95,12 @@ class Shipment(Document):
if flt(parcel.weight) <= 0: if flt(parcel.weight) <= 0:
frappe.throw(_("Parcel weight cannot be 0")) frappe.throw(_("Parcel weight cannot be 0"))
def set_total_weight(self):
self.total_weight = self.get_total_weight()
def get_total_weight(self):
return sum(flt(parcel.weight) * parcel.count for parcel in self.shipment_parcel if parcel.count > 0)
def validate_pickup_time(self): def validate_pickup_time(self):
if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from): if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from):
frappe.throw(_("Pickup To time should be greater than Pickup From time")) frappe.throw(_("Pickup To time should be greater than Pickup From time"))

View File

@@ -20,6 +20,17 @@ class TestShipment(FrappeTestCase):
self.assertEqual(len(second_shipment.shipment_delivery_note), 1) self.assertEqual(len(second_shipment.shipment_delivery_note), 1)
self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name) self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name)
def test_get_total_weight(self):
shipment = frappe.new_doc("Shipment")
shipment.extend(
"shipment_parcel",
[
{"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5},
{"length": 5, "width": 5, "height": 5, "weight": 10, "count": 1},
],
)
self.assertEqual(shipment.get_total_weight(), 35)
def create_test_delivery_note(): def create_test_delivery_note():
company = get_shipment_company() company = get_shipment_company()

View File

@@ -0,0 +1,9 @@
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import (
on_doctype_update as create_sle_indexes,
)
def execute():
"""Ensure SLE Indexes"""
create_sle_indexes()

View File

@@ -46,18 +46,20 @@
"auto_reserve_serial_and_batch", "auto_reserve_serial_and_batch",
"serial_and_batch_item_settings_tab", "serial_and_batch_item_settings_tab",
"section_break_7", "section_break_7",
"allow_existing_serial_no",
"do_not_use_batchwise_valuation", "do_not_use_batchwise_valuation",
"auto_create_serial_and_batch_bundle_for_outward", "auto_create_serial_and_batch_bundle_for_outward",
"pick_serial_and_batch_based_on", "pick_serial_and_batch_based_on",
"naming_series_prefix",
"column_break_mhzc", "column_break_mhzc",
"disable_serial_no_and_batch_selector", "disable_serial_no_and_batch_selector",
"use_naming_series",
"use_serial_batch_fields", "use_serial_batch_fields",
"do_not_update_serial_batch_on_creation_of_auto_bundle", "do_not_update_serial_batch_on_creation_of_auto_bundle",
"allow_existing_serial_no",
"serial_and_batch_bundle_section", "serial_and_batch_bundle_section",
"set_serial_and_batch_bundle_naming_based_on_naming_series", "set_serial_and_batch_bundle_naming_based_on_naming_series",
"section_break_gnhq",
"use_naming_series",
"column_break_wslv",
"naming_series_prefix",
"stock_planning_tab", "stock_planning_tab",
"auto_material_request", "auto_material_request",
"auto_indent", "auto_indent",
@@ -480,6 +482,14 @@
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series", "fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Set Serial and Batch Bundle Naming Based on Naming Series" "label": "Set Serial and Batch Bundle Naming Based on Naming Series"
},
{
"fieldname": "section_break_gnhq",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_wslv",
"fieldtype": "Column Break"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@@ -487,7 +497,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-02-17 14:36:36.177743", "modified": "2025-02-28 16:08:35.938840",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@@ -221,7 +221,7 @@ def update_stock(ctx, out, doc=None):
else: else:
qty -= batch_qty qty -= batch_qty
out.update({"batch_no": batch_no, "actual_batch_qty": qty}) out.update({"batch_no": batch_no, "actual_batch_qty": batch_qty})
if rate: if rate:
out.update({"rate": rate, "price_list_rate": rate}) out.update({"rate": rate, "price_list_rate": rate})
@@ -1051,7 +1051,11 @@ def get_batch_based_item_price(params, item_code) -> float:
if not item_price: if not item_price:
item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True) item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True)
if item_price and item_price[0][2] == params.get("uom"): if (
item_price
and item_price[0][2] == params.get("uom")
and not params.get("items", [{}])[0].get("is_free_item", 0)
):
return item_price[0][1] return item_price[0][1]
return 0.0 return 0.0

View File

@@ -51,6 +51,10 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
latest_age = date_diff(to_date, fifo_queue[-1][1]) latest_age = date_diff(to_date, fifo_queue[-1][1])
range_values = get_range_age(filters, fifo_queue, to_date, item_dict) range_values = get_range_age(filters, fifo_queue, to_date, item_dict)
check_and_replace_valuations_if_moving_average(
range_values, details.valuation_method, details.valuation_rate
)
row = [details.name, details.item_name, details.description, details.item_group, details.brand] row = [details.name, details.item_name, details.description, details.item_group, details.brand]
if filters.get("show_warehouse_wise_stock"): if filters.get("show_warehouse_wise_stock"):
@@ -72,6 +76,15 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
return data return data
def check_and_replace_valuations_if_moving_average(range_values, item_valuation_method, valuation_rate):
if item_valuation_method == "Moving Average" or (
not item_valuation_method
and frappe.db.get_single_value("Stock Settings", "valuation_method") == "Moving Average"
):
for i in range(0, len(range_values), 2):
range_values[i + 1] = range_values[i] * valuation_rate
def get_average_age(fifo_queue: list, to_date: str) -> float: def get_average_age(fifo_queue: list, to_date: str) -> float:
batch_age = age_qty = total_qty = 0.0 batch_age = age_qty = total_qty = 0.0
for batch in fifo_queue: for batch in fifo_queue:
@@ -267,7 +280,7 @@ class FIFOSlots:
self.__update_balances(d, key) self.__update_balances(d, key)
# Note that stock_ledger_entries is an iterator, you can not reuse it like a list # Note that stock_ledger_entries is an iterator, you can not reuse it like a list
del stock_ledger_entries del stock_ledger_entries
if not self.filters.get("show_warehouse_wise_stock"): if not self.filters.get("show_warehouse_wise_stock"):
@@ -396,6 +409,7 @@ class FIFOSlots:
self.item_details[key]["total_qty"] += row.actual_qty self.item_details[key]["total_qty"] += row.actual_qty
self.item_details[key]["has_serial_no"] = row.has_serial_no self.item_details[key]["has_serial_no"] = row.has_serial_no
self.item_details[key]["details"].valuation_rate = row.valuation_rate
def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict: def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict:
"Aggregate Item-Wh wise data into single Item entry." "Aggregate Item-Wh wise data into single Item entry."
@@ -437,8 +451,10 @@ class FIFOSlots:
item.description, item.description,
item.stock_uom, item.stock_uom,
item.has_serial_no, item.has_serial_no,
item.valuation_method,
sle.actual_qty, sle.actual_qty,
sle.stock_value_difference, sle.stock_value_difference,
sle.valuation_rate,
sle.posting_date, sle.posting_date,
sle.voucher_type, sle.voucher_type,
sle.voucher_no, sle.voucher_no,
@@ -506,7 +522,14 @@ class FIFOSlots:
item_table = frappe.qb.DocType("Item") item_table = frappe.qb.DocType("Item")
item = frappe.qb.from_("Item").select( item = frappe.qb.from_("Item").select(
"name", "item_name", "description", "stock_uom", "brand", "item_group", "has_serial_no" "name",
"item_name",
"description",
"stock_uom",
"brand",
"item_group",
"has_serial_no",
"valuation_method",
) )
if self.filters.get("item_code"): if self.filters.get("item_code"):

View File

@@ -1006,6 +1006,10 @@ class SerialBatchCreation:
elif self.has_serial_no and not self.get("serial_nos"): elif self.has_serial_no and not self.get("serial_nos"):
self.serial_nos = get_serial_nos_for_outward(kwargs) self.serial_nos = get_serial_nos_for_outward(kwargs)
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"): elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
if self.get("posting_date"):
kwargs["posting_date"] = self.get("posting_date")
kwargs["posting_time"] = self.get("posting_time")
self.batches = get_available_batches(kwargs) self.batches = get_available_batches(kwargs)
def set_auto_serial_batch_entries_for_inward(self): def set_auto_serial_batch_entries_for_inward(self):

View File

@@ -561,12 +561,28 @@ class update_entries_after:
self.new_items_found = False self.new_items_found = False
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
self.affected_transactions: set[tuple[str, str]] = set() self.affected_transactions: set[tuple[str, str]] = set()
self.reserved_stock = flt(self.args.reserved_stock) self.reserved_stock = self.get_reserved_stock()
self.data = frappe._dict() self.data = frappe._dict()
self.initialize_previous_data(self.args) self.initialize_previous_data(self.args)
self.build() self.build()
def get_reserved_stock(self):
sre = frappe.qb.DocType("Stock Reservation Entry")
posting_datetime = get_combine_datetime(self.args.posting_date, self.args.posting_time)
query = (
frappe.qb.from_(sre)
.select(Sum(sre.reserved_qty) - Sum(sre.delivered_qty))
.where(
(sre.item_code == self.item_code)
& (sre.warehouse == self.args.warehouse)
& (sre.docstatus == 1)
& (sre.creation <= posting_datetime)
)
).run()
return flt(query[0][0]) if query else 0.0
def set_precision(self): def set_precision(self):
self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
self.currency_precision = get_field_precision( self.currency_precision = get_field_precision(

Some files were not shown because too many files have changed in this diff Show More