Merge branch 'develop' into st31369

This commit is contained in:
Mihir Kandoi
2025-03-05 12:13:13 +05:30
committed by GitHub
78 changed files with 10476 additions and 9275 deletions

View File

@@ -2,29 +2,27 @@ pull_request_rules:
- name: Auto-close PRs on stable branch
conditions:
- and:
- and:
- author!=surajshetty3416
- author!=gavindsouza
- author!=rohitwaghchaure
- author!=nabinhait
- author!=ankush
- author!=deepeshgarg007
- author!=frappe-pr-bot
- author!=mergify[bot]
- or:
- base=version-13
- base=version-12
- base=version-14
- base=version-15
- base=version-16
- and:
- author!=surajshetty3416
- author!=gavindsouza
- author!=rohitwaghchaure
- author!=nabinhait
- author!=ankush
- author!=deepeshgarg007
- author!=frappe-pr-bot
- author!=mergify[bot]
- or:
- base=version-13
- base=version-12
- base=version-14
- base=version-15
- base=version-16
actions:
close:
comment:
message: |
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
message: |
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
- name: backport to develop
conditions:
- label="backport develop"
@@ -34,7 +32,6 @@ pull_request_rules:
- develop
assignees:
- "{{ author }}"
- name: backport to version-14-hotfix
conditions:
- label="backport version-14-hotfix"
@@ -44,7 +41,6 @@ pull_request_rules:
- version-14-hotfix
assignees:
- "{{ author }}"
- name: backport to version-15-hotfix
conditions:
- label="backport version-15-hotfix"
@@ -54,18 +50,6 @@ pull_request_rules:
- version-15-hotfix
assignees:
- "{{ author }}"
- name: backport to version-13-hotfix
conditions:
- label="backport version-13-hotfix"
actions:
backport:
branches:
- version-13-hotfix
assignees:
- "{{ author }}"
- name: Automatic merge on CI success and review
conditions:
- status-success=linters
@@ -96,6 +80,6 @@ pull_request_rules:
merge:
method: squash
commit_message_template: |
{{ title }} (#{{ number }})
{{ title }} (#{{ number }})
{{ body }}
{{ body }}

View File

@@ -1,5 +1,5 @@
<div align="center">
<a href="https://erpnext.com">
<a href="https://frappe.io/erpnext">
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
</a>
<h2>ERPNext</h2>
@@ -19,9 +19,9 @@
<div align="center">
<a href="https://erpnext-demo.frappe.cloud/app/home">Live Demo</a>
-
<a href="https://erpnext.com">Website</a>
<a href="https://frappe.io/erpnext">Website</a>
-
<a href="https://docs.erpnext.com">Documentation</a>
<a href="https://docs.frappe.io/erpnext/">Documentation</a>
</div>
## ERPNext
@@ -115,16 +115,16 @@ To setup the repository locally follow the steps mentioned below:
```
# Create a new site
bench new-site erpnext.dev
# Map your site to localhost
bench --site erpnext.dev add-to-hosts
```
3. Get the ERPNext app and install it
```
# Get the ERPNext app
bench get-app https://github.com/frappe/erpnext
# Install the app
bench --site erpnext.dev install-app erpnext
```

View File

@@ -4,7 +4,7 @@
import frappe
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
import erpnext
@@ -479,6 +479,7 @@ def get_account_autoname(account_number, account_name, company):
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False):
_ensure_idle_system()
account = frappe.get_cached_doc("Account", name)
if not account:
return
@@ -540,6 +541,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new):
_ensure_idle_system()
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old)
@@ -593,3 +595,27 @@ def sync_update_account_number_in_child(
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)
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;
},
validate: async function (frm) {
await frm.events.set_exchange_gain_loss_deduction(frm);
},
validate_company: (frm) => {
if (!frm.doc.company) {
frappe.throw({ message: __("Please select a Company first."), title: __("Mandatory") });
@@ -1837,8 +1841,6 @@ function prompt_for_missing_account(frm, account) {
(values) => resolve(values?.[account]),
__("Please Specify Account")
);
dialog.on_hide = () => resolve("");
});
}

View File

@@ -1942,7 +1942,7 @@ class PaymentEntry(AccountsController):
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 total_negative_outstanding == 0:
frappe.msgprint(
@@ -3437,13 +3437,14 @@ def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
"""Add loss on income discount in base currency."""
precision = doc.precision("total")
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
positive_negative = -1 if pe.payment_type == "Pay" else 1
pe.append(
"deductions",
{
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": flt(base_loss_on_income, precision),
"amount": flt(base_loss_on_income, precision) * positive_negative,
},
)
@@ -3455,6 +3456,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
tax_discount_loss = {}
base_total_tax_loss = 0
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
for tax in doc.get("taxes", []):
@@ -3477,7 +3479,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
"account": account,
"cost_center": pe.cost_center
or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": flt(loss, precision),
"amount": flt(loss, precision) * positive_negative,
},
)

View File

@@ -291,6 +291,48 @@ class TestPaymentEntry(IntegrationTestCase):
self.assertEqual(si.payment_schedule[0].paid_amount, 200.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):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template_with_discount()

View File

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

View File

@@ -895,8 +895,16 @@ frappe.ui.form.on("Sales Invoice", {
project: function (frm) {
if (frm.doc.project) {
frm.events.add_timesheet_data(frm, {
project: frm.doc.project,
frappe.call({
method: "is_auto_fetch_timesheet_enabled",
doc: frm.doc,
callback: function (r) {
if (cint(r.message)) {
frm.events.add_timesheet_data(frm, {
project: frm.doc.project,
});
}
},
});
}
},

View File

@@ -1096,16 +1096,17 @@ class SalesInvoice(SellingController):
timesheet.billing_amount = ts_doc.total_billable_amount
def update_timesheet_billing_for_project(self):
if self.timesheets:
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
self.add_timesheet_data()
else:
self.calculate_billing_amount_for_timesheet()
@frappe.whitelist(methods=["PUT"])
def add_timesheet_data(self):
if not self.timesheets and self.project:
self._add_timesheet_data()
self.save()
@frappe.whitelist()
def is_auto_fetch_timesheet_enabled(self):
return frappe.db.get_single_value("Projects Settings", "fetch_timesheet_in_sales_invoice")
def _add_timesheet_data(self):
@frappe.whitelist()
def add_timesheet_data(self):
self.set("timesheets", [])
if self.project:
for data in get_projectwise_timesheet_data(self.project):

View File

@@ -4305,6 +4305,31 @@ class TestSalesInvoice(IntegrationTestCase):
doc = frappe.get_doc("Project", project.name)
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):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return

View File

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

View File

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

View File

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

View File

@@ -38,6 +38,23 @@ class TestAccountsPayable(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(data[1][0].get("outstanding"), 300)
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):
frappe.set_user("Administrator")
pi = make_purchase_invoice(

View File

@@ -267,6 +267,18 @@ class ReceivablePayableReport:
row.invoiced_in_account_currency += amount_in_account_currency
else:
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:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
@@ -421,7 +433,7 @@ class ReceivablePayableReport:
# nosemgrep
si_list = frappe.db.sql(
"""
select name, due_date, po_no
select name, due_date, po_no, is_return
from `tabSales Invoice`
where posting_date <= %s
and company = %s
@@ -453,7 +465,7 @@ class ReceivablePayableReport:
# nosemgrep
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`
where
posting_date <= %s

View File

@@ -204,7 +204,7 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
expected_data_after_credit_note = [
[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)
si_row = next(
@@ -478,13 +478,19 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
report = execute(filters)[1]
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]
for row in rows:
self.assertEqual(
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()

View File

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

View File

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

View File

@@ -652,6 +652,9 @@ frappe.ui.form.on("Asset", {
frm.set_value("purchase_amount", data.gross_purchase_amount);
frm.set_value("asset_quantity", data.asset_quantity);
frm.set_value("cost_center", data.cost_center);
if (data.asset_location) {
frm.set_value("location", data.asset_location);
}
if (doctype === "Purchase Receipt") {
frm.set_value("purchase_receipt_item", data.purchase_receipt_item);

View File

@@ -19,6 +19,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
on_change: (report) => {
report.set_filter_value("name", []);
report.refresh();
},
},
{
fieldname: "to_date",
@@ -27,6 +31,10 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.get_today(),
on_change: (report) => {
report.set_filter_value("name", []);
report.refresh();
},
},
{
fieldname: "project",
@@ -38,13 +46,17 @@ frappe.query_reports["Purchase Order Analysis"] = {
{
fieldname: "name",
label: __("Purchase Order"),
fieldtype: "Link",
fieldtype: "MultiSelectList",
width: "80",
options: "Purchase Order",
get_query: () => {
return {
filters: { docstatus: 1 },
};
get_data: function (txt) {
let 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"),
fieldtype: "MultiSelectList",
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) {
let status = ["To Pay", "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 = [];
for (let option of status) {
options.push({

View File

@@ -70,14 +70,16 @@ def get_data(filters):
po.company,
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)
.orderby(po.transaction_date)
)
for field in ("company", "name"):
if filters.get(field):
query = query.where(po[field] == filters.get(field))
if filters.get("company"):
query = query.where(po.company == filters.get("company"))
if filters.get("name"):
query = query.where(po.name.isin(filters.get("name")))
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")))

View File

@@ -197,6 +197,14 @@ class AccountsController(TransactionBase):
self.set_incoming_rate()
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"):
self.calculate_taxes_and_totals()
@@ -207,10 +215,6 @@ class AccountsController(TransactionBase):
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_currency()
self.validate_party_account_currency()
@@ -255,8 +259,6 @@ class AccountsController(TransactionBase):
self.validate_deferred_income_expense_account()
self.set_inter_company_account()
self.set_taxes_and_charges()
if self.doctype == "Purchase Invoice":
self.calculate_paid_amount()
# apply tax withholding only if checked and applicable
@@ -823,11 +825,15 @@ class AccountsController(TransactionBase):
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)
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(
fieldname
):

View File

@@ -935,6 +935,35 @@ class TestAccountsController(IntegrationTestCase):
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, [])
@IntegrationTestCase.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):
# Invoice in Foreign Currency
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_icon = "fa fa-th"
app_color = "#e74c3c"
app_email = "info@erpnext.com"
app_email = "hello@frappe.io"
app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext"
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
@@ -479,7 +479,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg"
default_mail_footer = """
<span>
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
</a>
</span>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -403,3 +403,4 @@ erpnext.patches.v14_0.disable_add_row_in_gross_profit
erpnext.patches.v14_0.update_posting_datetime
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
erpnext.patches.v15_0.recalculate_amount_difference_field
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes

View File

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

View File

@@ -322,17 +322,31 @@ class Project(Document):
self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0
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(
"""select sum(base_net_amount)
from `tabSales Invoice Item` si_item, `tabSales Invoice` si
where si_item.parent = si.name
and if(si_item.project, si_item.project, si.project) = %s
and si.docstatus=1""",
from `tabSales Invoice` si join `tabSales Invoice Item` si_item on si_item.parent = si.name
where si_item.project is null
and si.project is not null
and si.project = %s
and si.docstatus = 1""",
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):
if old_name == self.copied_from:

View File

@@ -8,7 +8,8 @@
"timesheet_sb",
"ignore_workstation_time_overlap",
"ignore_user_time_overlap",
"ignore_employee_time_overlap"
"ignore_employee_time_overlap",
"fetch_timesheet_in_sales_invoice"
],
"fields": [
{
@@ -33,11 +34,18 @@
"fieldname": "ignore_employee_time_overlap",
"fieldtype": "Check",
"label": "Ignore Employee Time Overlap"
},
{
"default": "0",
"description": "Enabling the check box will fetch timesheet on select of a Project in Sales Invoice",
"fieldname": "fetch_timesheet_in_sales_invoice",
"fieldtype": "Check",
"label": "Fetch Timesheet in Sales Invoice"
}
],
"issingle": 1,
"links": [],
"modified": "2024-03-27 13:10:21.984404",
"modified": "2025-02-13 23:01:27.321902",
"modified_by": "Administrator",
"module": "Projects",
"name": "Projects Settings",

View File

@@ -14,6 +14,7 @@ class ProjectsSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
fetch_timesheet_in_sales_invoice: DF.Check
ignore_employee_time_overlap: DF.Check
ignore_user_time_overlap: DF.Check
ignore_workstation_time_overlap: DF.Check

View File

@@ -53,6 +53,7 @@ class TestTimesheet(IntegrationTestCase):
self.assertEqual(item.qty, 2.00)
self.assertEqual(item.rate, 50.00)
@IntegrationTestCase.change_settings("Projects Settings", {"fetch_timesheet_in_sales_invoice": 1})
def test_timesheet_billing_based_on_project(self):
emp = make_employee("test_employee_6@salary.com")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
@@ -62,7 +63,7 @@ class TestTimesheet(IntegrationTestCase):
)
sales_invoice = create_sales_invoice(do_not_save=True)
sales_invoice.project = project
sales_invoice._add_timesheet_data()
sales_invoice.add_timesheet_data()
sales_invoice.submit()
ts = frappe.get_doc("Timesheet", timesheet.name)

View File

@@ -1373,6 +1373,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
() => this.calculate_stock_uom_rate(doc, cdt, cdn),
() => this.apply_pricing_rule(item, true)
]);
} else {
this.conversion_factor(doc, cdt, cdn, true)
}
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
{{ address_line1 }}<br>
{% 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 -%}

View File

@@ -2185,6 +2185,45 @@ class TestSalesOrder(AccountsTestMixin, IntegrationTestCase):
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)
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):
# set credit limit
company = "_Test Company"

View File

@@ -952,7 +952,6 @@
"options": "Cost Center",
"print_hide": 1,
"print_width": "120px",
"reqd": 1,
"width": "120px"
},
{
@@ -971,7 +970,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-02-06 13:29:24.619850",
"modified": "2025-02-28 09:45:43.934947",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@@ -32,7 +32,7 @@ class SalesOrderItem(Document):
brand: DF.Link | None
company_total_stock: DF.Float
conversion_factor: DF.Float
cost_center: DF.Link
cost_center: DF.Link | None
customer_item_code: DF.Data | None
delivered_by_supplier: DF.Check
delivered_qty: DF.Float

View File

@@ -320,13 +320,13 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = []
if search_term and status:
invoices_by_customer = frappe.db.get_all(
invoices_by_customer = frappe.db.get_list(
"POS Invoice",
filters={"customer": ["like", f"%{search_term}%"], "status": status},
fields=fields,
page_length=limit,
)
invoices_by_name = frappe.db.get_all(
invoices_by_name = frappe.db.get_list(
"POS Invoice",
filters={"name": ["like", f"%{search_term}%"], "status": status},
fields=fields,
@@ -335,7 +335,7 @@ def get_past_order_list(search_term, status, limit=20):
invoice_list = invoices_by_customer + invoices_by_name
elif status:
invoice_list = frappe.db.get_all(
invoice_list = frappe.db.get_list(
"POS Invoice", filters={"status": status}, fields=fields, page_length=limit
)

View File

@@ -40,7 +40,7 @@ erpnext.PointOfSale.Controller = class {
in_list_view: 1,
label: __("Opening Amount"),
options: "company:company_currency",
change: function () {
onchange: function () {
dialog.fields_dict.balance_details.df.data.some((d) => {
if (d.idx == this.doc.idx) {
d.opening_amount = this.value;
@@ -609,6 +609,14 @@ erpnext.PointOfSale.Controller = class {
if (this.is_current_item_being_edited(item_row) || from_selector) {
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);
}
} else {

View File

@@ -187,6 +187,7 @@ erpnext.PointOfSale.ItemDetails = class {
this[`${fieldname}_control`].set_value(item[fieldname]);
});
this.resize_serial_control(item);
this.make_auto_serial_selection_btn(item);
this.bind_custom_control_change_event();
@@ -203,11 +204,17 @@ erpnext.PointOfSale.ItemDetails = class {
"actual_qty",
"price_list_rate",
];
if (item.has_serial_no) fields.push("serial_no");
if (item.has_batch_no) fields.push("batch_no");
if (item.has_serial_no || item.serial_no) fields.push("serial_no");
if (item.has_batch_no || item.batch_no) fields.push("batch_no");
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) {
if (item.has_serial_no || item.has_batch_no) {
if (item.has_serial_no && item.has_batch_no) {
@@ -225,7 +232,6 @@ erpnext.PointOfSale.ItemDetails = class {
`<div class="btn btn-sm btn-secondary auto-fetch-btn" style="margin-top: 6px">${label}</div>`
);
}
this.$form_container.find(".serial_no-control").find("textarea").css("height", "6rem");
}
}

View File

@@ -340,19 +340,11 @@ erpnext.PointOfSale.Payment = class {
// pass
}
async render_payment_section() {
render_payment_section() {
this.render_payment_mode_dom();
this.make_invoice_fields_control();
this.update_totals_section();
let r = await frappe.db.get_value(
"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();
}
this.unset_grand_total_to_default_mop();
}
after_render() {
@@ -637,6 +629,19 @@ erpnext.PointOfSale.Payment = class {
.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() {
const doc = this.events.get_frm().doc;
let validation_flag = true;

View File

@@ -19,6 +19,10 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
reqd: 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",
@@ -27,6 +31,10 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
reqd: 1,
default: frappe.datetime.get_today(),
on_change: (report) => {
report.set_filter_value("sales_order", []);
report.refresh();
},
},
{
fieldname: "sales_order",
@@ -35,12 +43,13 @@ frappe.query_reports["Sales Order Analysis"] = {
width: "80",
options: "Sales Order",
get_data: function (txt) {
return frappe.db.get_link_options("Sales Order", txt);
},
get_query: () => {
return {
filters: { docstatus: 1 },
};
let 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("Sales Order", txt, filters);
},
},
{
@@ -53,10 +62,17 @@ frappe.query_reports["Sales Order Analysis"] = {
fieldname: "status",
label: __("Status"),
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",
get_data: function (txt) {
let status = ["To Pay", "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 = [];
for (let option of status) {
options.push({

View File

@@ -86,7 +86,7 @@ def get_data(conditions, filters):
ON sii.so_detail = soi.name and sii.docstatus = 1
WHERE
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
{conditions}
GROUP BY soi.name

View File

@@ -14,7 +14,7 @@ from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
from .default_success_action import get_default_success_action
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():

View File

@@ -365,6 +365,8 @@ class DeprecatedBatchNoValuation:
if self.sle.voucher_detail_no:
query = query.where(sabb.voucher_detail_no != self.sle.voucher_detail_no)
query = query.where(sabb.voucher_type != "Pick List")
data = query.run(as_dict=True)
if not data:
return {}

View File

@@ -247,7 +247,7 @@ def update_qty(bin_name, args):
& (sle.warehouse == args.get("warehouse"))
& (sle.is_cancelled == 0)
)
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc)
.orderby(sle.posting_datetime, order=Order.desc)
.orderby(sle.creation, order=Order.desc)
.limit(1)
.run()

View File

@@ -236,7 +236,12 @@ class InventoryDimension(Document):
custom_fields[dt] = dimension_field
filter_custom_fields = {}
ignore_doctypes = ["Serial and Batch Bundle", "Serial and Batch Entry", "Pick List Item"]
ignore_doctypes = [
"Serial and Batch Bundle",
"Serial and Batch Entry",
"Pick List Item",
"Maintenance Visit Purpose",
]
if custom_fields:
for doctype, fields in custom_fields.items():

View File

@@ -1081,15 +1081,19 @@ def get_billed_amount_against_po(po_items):
if not po_items:
return {}
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
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)
.where(
(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.update_stock == 0)
)
.groupby(purchase_invoice_item.po_detail)
).run(as_dict=1)

View File

@@ -4096,6 +4096,36 @@ class TestPurchaseReceipt(IntegrationTestCase):
batch_return.save()
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():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

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

View File

@@ -65,6 +65,7 @@ class Shipment(Document):
shipment_parcel: DF.Table[ShipmentParcel]
shipment_type: DF.Literal["Goods", "Documents"]
status: DF.Literal["Draft", "Submitted", "Booked", "Cancelled", "Completed"]
total_weight: DF.Float
tracking_status: DF.Literal["", "In Progress", "Delivered", "Returned", "Lost"]
tracking_status_info: DF.Data | None
tracking_url: DF.SmallText | None
@@ -75,6 +76,7 @@ class Shipment(Document):
self.validate_weight()
self.validate_pickup_time()
self.set_value_of_goods()
self.set_total_weight()
if self.docstatus == 0:
self.status = "Draft"
@@ -93,6 +95,12 @@ class Shipment(Document):
if flt(parcel.weight) <= 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):
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"))

View File

@@ -29,6 +29,17 @@ class TestShipment(IntegrationTestCase):
self.assertEqual(len(second_shipment.shipment_delivery_note), 1)
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():
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

@@ -47,18 +47,20 @@
"auto_reserve_serial_and_batch",
"serial_and_batch_item_settings_tab",
"section_break_7",
"allow_existing_serial_no",
"do_not_use_batchwise_valuation",
"auto_create_serial_and_batch_bundle_for_outward",
"pick_serial_and_batch_based_on",
"naming_series_prefix",
"column_break_mhzc",
"disable_serial_no_and_batch_selector",
"use_naming_series",
"use_serial_batch_fields",
"do_not_update_serial_batch_on_creation_of_auto_bundle",
"allow_existing_serial_no",
"serial_and_batch_bundle_section",
"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",
"auto_material_request",
"auto_indent",
@@ -488,6 +490,14 @@
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series",
"fieldtype": "Check",
"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",
@@ -495,7 +505,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-02-17 13:36:36.177743",
"modified": "2025-02-28 15:08:35.938840",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

View File

@@ -227,7 +227,7 @@ def update_stock(ctx, out, doc=None):
else:
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:
out.update({"rate": rate, "price_list_rate": rate})
@@ -1095,7 +1095,9 @@ def get_batch_based_item_price(pctx: ItemPriceCtx | dict | str, item_code) -> fl
if not item_price:
item_price = get_item_price(pctx, item_code, ignore_party=True, force_batch_no=True)
if item_price and item_price[0].uom == pctx.uom:
is_free_item = pctx.get("items", [{}])[0].get("is_free_item")
if item_price and item_price[0].uom == pctx.uom and not is_free_item:
return item_price[0].price_list_rate
return 0.0

View File

@@ -179,7 +179,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
sle = frappe.qb.DocType("Stock Ledger Entry")
batch_package = frappe.qb.DocType("Serial and Batch Entry")
to_date = get_datetime(filters.to_date + " 23:59:59")
to_date = get_datetime(str(filters.to_date) + " 23:59:59")
query = (
frappe.qb.from_(sle)

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])
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]
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
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:
batch_age = age_qty = total_qty = 0.0
for batch in fifo_queue:
@@ -267,7 +280,7 @@ class FIFOSlots:
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
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]["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:
"Aggregate Item-Wh wise data into single Item entry."
@@ -437,8 +451,10 @@ class FIFOSlots:
item.description,
item.stock_uom,
item.has_serial_no,
item.valuation_method,
sle.actual_qty,
sle.stock_value_difference,
sle.valuation_rate,
sle.posting_date,
sle.voucher_type,
sle.voucher_no,
@@ -506,7 +522,14 @@ class FIFOSlots:
item_table = frappe.qb.DocType("Item")
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"):

View File

@@ -1006,6 +1006,10 @@ class SerialBatchCreation:
elif self.has_serial_no and not self.get("serial_nos"):
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"):
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)
def set_auto_serial_batch_entries_for_inward(self):

View File

@@ -562,12 +562,28 @@ class update_entries_after:
self.new_items_found = False
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
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.initialize_previous_data(self.args)
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):
self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
self.currency_precision = get_field_precision(
@@ -884,6 +900,10 @@ class update_entries_after:
if not sle.is_adjustment_entry:
sle.stock_value_difference = stock_value_difference
elif sle.is_adjustment_entry and not self.args.get("sle_id"):
sle.stock_value_difference = get_stock_value_difference(
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no
)
sle.doctype = "Stock Ledger Entry"
frappe.get_doc(sle).db_update()

View File

@@ -1 +1 @@
{{ _("Powered by {0}").format('<a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}
{{ _("Powered by {0}").format('<a href="https://frappe.io/erpnext?source=website_footer" target="_blank" class="text-muted">ERPNext</a>') }}

View File

@@ -3,6 +3,7 @@
import frappe
from frappe.utils import escape_html
@frappe.whitelist(allow_guest=True)
@@ -11,6 +12,8 @@ def send_message(sender, message, subject="Website Query"):
website_send_message(sender, message, subject)
message = escape_html(message)
lead = customer = None
customer = frappe.db.sql(
"""select distinct dl.link_name from `tabDynamic Link` dl

View File

@@ -5,7 +5,7 @@
"type": "git",
"url": "git+https://github.com/frappe/erpnext.git"
},
"homepage": "https://erpnext.com",
"homepage": "https://frappe.io/erpnext",
"author": "Frappe Technologies Pvt. Ltd.",
"license": "GPL-3.0",
"bugs": {

View File

@@ -77,6 +77,6 @@ docstring-code-format = true
[project.urls]
Homepage = "https://erpnext.com/"
Homepage = "https://frappe.io/erpnext"
Repository = "https://github.com/frappe/erpnext.git"
"Bug Reports" = "https://github.com/frappe/erpnext/issues"