Merge branch 'develop' of https://github.com/frappe/erpnext into immutable_ledger

This commit is contained in:
Deepesh Garg
2024-05-13 17:22:27 +05:30
91 changed files with 17618 additions and 14612 deletions

View File

@@ -360,45 +360,45 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
)
if not amount:
return
gl_posting_date = end_date
prev_posting_date = None
# check if books nor frozen till endate:
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
prev_posting_date = end_date
if via_journal_entry:
book_revenue_via_journal_entry(
doc,
credit_account,
debit_account,
amount,
base_amount,
gl_posting_date,
project,
account_currency,
item.cost_center,
item,
deferred_process,
submit_journal_entry,
)
else:
make_gl_entries(
doc,
credit_account,
debit_account,
against,
amount,
base_amount,
gl_posting_date,
project,
account_currency,
item.cost_center,
item,
deferred_process,
)
gl_posting_date = end_date
prev_posting_date = None
# check if books nor frozen till endate:
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
prev_posting_date = end_date
if via_journal_entry:
book_revenue_via_journal_entry(
doc,
credit_account,
debit_account,
amount,
base_amount,
gl_posting_date,
project,
account_currency,
item.cost_center,
item,
deferred_process,
submit_journal_entry,
)
else:
make_gl_entries(
doc,
credit_account,
debit_account,
against,
amount,
base_amount,
gl_posting_date,
project,
account_currency,
item.cost_center,
item,
deferred_process,
)
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
if frappe.flags.deferred_accounting_error:

View File

@@ -65,6 +65,8 @@
"label": "Is Group"
},
{
"fetch_from": "parent_account.company",
"fetch_if_empty": 1,
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,

View File

@@ -106,7 +106,7 @@
},
{
"default": "0",
"description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field",
"description": "Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year",
"fieldname": "check_supplier_invoice_uniqueness",
"fieldtype": "Check",
"label": "Check Supplier Invoice Number Uniqueness"
@@ -498,4 +498,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -26,6 +26,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Company",
"options": "Company"
},
@@ -118,7 +119,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-03-27 13:06:37.922473",
"modified": "2024-04-28 14:40:50.910884",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Reconciliation Tool",

View File

@@ -719,7 +719,7 @@ def get_pe_matching_query(
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
ConstantColumn("Payment Entry").as_("doctype"),
pe.name,
pe.paid_amount,
pe.paid_amount_after_tax.as_("paid_amount"),
pe.reference_no,
pe.reference_date,
pe.party,

View File

@@ -197,6 +197,7 @@ class PaymentEntry(AccountsController):
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
return
self.book_advance_payments_in_separate_party_account = False
if self.party_type not in ("Customer", "Supplier"):
return
@@ -1225,88 +1226,71 @@ class PaymentEntry(AccountsController):
)
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
if self.book_advance_payments_in_separate_party_account:
for d in self.get("references"):
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
gle = party_gl_dict.copy()
if self.payment_type == "Receive":
amount = self.base_paid_amount
else:
amount = self.base_received_amount
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
reverse_dr_or_cr = 0
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
payable_party_types = get_party_types_from_account_type("Payable")
receivable_party_types = get_party_types_from_account_type("Receivable")
if (
is_return
and self.party_type in receivable_party_types
and (self.payment_type == "Pay")
):
reverse_dr_or_cr = 1
elif (
is_return
and self.party_type in payable_party_types
and (self.payment_type == "Receive")
):
reverse_dr_or_cr = 1
if is_return and not reverse_dr_or_cr:
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
exchange_rate = self.get_exchange_rate()
amount_in_account_currency = amount * exchange_rate
gle.update(
{
dr_or_cr: amount,
dr_or_cr + "_in_account_currency": amount_in_account_currency,
"against_voucher_type": "Payment Entry",
"against_voucher": self.name,
"cost_center": self.cost_center,
dr_or_cr: abs(allocated_amount_in_company_currency),
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
"cost_center": cost_center,
}
)
gl_entries.append(gle)
else:
for d in self.get("references"):
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(
d.reference_doctype, d.reference_name, "cost_center"
)
gle = party_gl_dict.copy()
if self.unallocated_amount:
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
exchange_rate = self.get_exchange_rate()
base_unallocated_amount = self.unallocated_amount * exchange_rate
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(
d
)
reverse_dr_or_cr = 0
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
payable_party_types = get_party_types_from_account_type("Payable")
receivable_party_types = get_party_types_from_account_type("Receivable")
if (
is_return
and self.party_type in receivable_party_types
and (self.payment_type == "Pay")
):
reverse_dr_or_cr = 1
elif (
is_return
and self.party_type in payable_party_types
and (self.payment_type == "Receive")
):
reverse_dr_or_cr = 1
if is_return and not reverse_dr_or_cr:
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
gle = party_gl_dict.copy()
gle.update(
{
dr_or_cr + "_in_account_currency": self.unallocated_amount,
dr_or_cr: base_unallocated_amount,
}
)
if self.book_advance_payments_in_separate_party_account:
gle.update(
{
dr_or_cr: abs(allocated_amount_in_company_currency),
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
"cost_center": cost_center,
"against_voucher_type": "Payment Entry",
"against_voucher": self.name,
}
)
gl_entries.append(gle)
if self.unallocated_amount:
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
exchange_rate = self.get_exchange_rate()
base_unallocated_amount = self.unallocated_amount * exchange_rate
gle = party_gl_dict.copy()
gle.update(
{
dr_or_cr + "_in_account_currency": self.unallocated_amount,
dr_or_cr: base_unallocated_amount,
}
)
gl_entries.append(gle)
gl_entries.append(gle)
def make_advance_gl_entries(
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"
@@ -1321,7 +1305,7 @@ class PaymentEntry(AccountsController):
def add_advance_gl_entries(self, gl_entries: list, entry: object | dict | None):
"""
If 'entry' is passed, GL enties only for that reference is added.
If 'entry' is passed, GL entries only for that reference is added.
"""
if self.book_advance_payments_in_separate_party_account:
references = [x for x in self.get("references")]
@@ -1333,8 +1317,6 @@ class PaymentEntry(AccountsController):
"Sales Invoice",
"Purchase Invoice",
"Journal Entry",
"Sales Order",
"Purchase Order",
"Payment Entry",
):
self.add_advance_gl_for_reference(gl_entries, ref)

View File

@@ -1477,6 +1477,68 @@ class TestPaymentEntry(FrappeTestCase):
self.check_gl_entries()
self.check_pl_entries()
def test_advance_as_liability_against_order(self):
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as _make_purchase_invoice,
)
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
company = "_Test Company"
advance_account = create_account(
parent_account="Current Liabilities - _TC",
account_name="Advances Paid",
company=company,
account_type="Liability",
)
frappe.db.set_value(
"Company",
company,
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_paid_account": advance_account,
},
)
po = create_purchase_order(supplier="_Test Supplier")
pe = get_payment_entry("Purchase Order", po.name, bank_account="Cash - _TC")
pe.save().submit()
pre_reconciliation_gle = [
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
]
self.voucher_no = pe.name
self.expected_gle = pre_reconciliation_gle
self.check_gl_entries()
# Make Purchase Invoice against the order
pi = _make_purchase_invoice(po.name)
pi.append(
"advances",
{
"reference_type": pe.doctype,
"reference_name": pe.name,
"reference_row": pe.references[0].name,
"advance_amount": 5000,
"allocated_amount": 5000,
},
)
pi.save().submit()
# # assert General and Payment Ledger entries post partial reconciliation
self.expected_gle = [
{"account": pi.credit_to, "debit": 5000.0, "credit": 0.0},
{"account": "Cash - _TC", "debit": 0.0, "credit": 5000.0},
{"account": advance_account, "debit": 5000.0, "credit": 0.0},
{"account": advance_account, "debit": 0.0, "credit": 5000.0},
]
self.voucher_no = pe.name
self.check_gl_entries()
def check_pl_entries(self):
ple = frappe.qb.DocType("Payment Ledger Entry")
pl_entries = (

View File

@@ -176,8 +176,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
},
callback: (r) => {
if (!r.exc && r.message) {
this.frm.set_value("receivable_payable_account", r.message[0]);
this.frm.set_value("default_advance_account", r.message[1]);
if (typeof r.message === "string") {
this.frm.set_value("receivable_payable_account", r.message);
} else if (Array.isArray(r.message)) {
this.frm.set_value("receivable_payable_account", r.message[0]);
this.frm.set_value("default_advance_account", r.message[1]);
}
}
this.frm.refresh();
},

View File

@@ -573,6 +573,22 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
if pricing_rule.apply_discount_on_rate and item_details.get("discount_percentage"):
# Apply discount on discounted rate
item_details[field] += (100 - item_details[field]) * (pricing_rule.get(field, 0) / 100)
elif args.price_list_rate:
value = pricing_rule.get(field, 0)
calculate_discount_percentage = False
if field == "discount_percentage":
field = "discount_amount"
value = args.price_list_rate * (value / 100)
calculate_discount_percentage = True
if field not in item_details:
item_details.setdefault(field, 0)
item_details[field] += value if pricing_rule else args.get(field, 0)
if calculate_discount_percentage and args.price_list_rate and item_details.discount_amount:
item_details.discount_percentage = flt(
(flt(item_details.discount_amount) / flt(args.price_list_rate)) * 100
)
else:
if field not in item_details:
item_details.setdefault(field, 0)

View File

@@ -1102,7 +1102,60 @@ class TestPricingRule(unittest.TestCase):
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 4)
self.assertEqual(so.items[1].qty, 3)
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule 1",
"name": "_Test Pricing Rule 1",
"apply_on": "Item Code",
"currency": "USD",
"items": [
{
"item_code": "_Test Item",
}
],
"selling": 1,
"price_or_product_discount": "Price",
"rate_or_discount": "Discount Percentage",
"discount_percentage": 10,
"apply_multiple_pricing_rules": 1,
"company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule 2",
"name": "_Test Pricing Rule 2",
"apply_on": "Item Code",
"currency": "USD",
"items": [
{
"item_code": "_Test Item",
}
],
"selling": 1,
"price_or_product_discount": "Price",
"rate_or_discount": "Discount Amount",
"discount_amount": 100,
"apply_multiple_pricing_rules": 1,
"company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
so = make_sales_order(item_code="_Test Item", qty=1, price_list_rate=1000, do_not_submit=True)
self.assertEqual(so.items[0].discount_amount, 200)
self.assertEqual(so.items[0].rate, 800)
frappe.delete_doc_if_exists("Sales Order", so.name)
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
test_dependencies = ["Campaign"]

View File

@@ -6,6 +6,7 @@
import copy
import json
import math
import frappe
from frappe import _, bold
@@ -653,7 +654,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if transaction_qty:
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
if pricing_rule.round_free_qty:
qty = round(qty)
qty = math.floor(qty)
free_item_data_args = {
"item_code": free_item,

View File

@@ -158,7 +158,7 @@ def set_ageing(doc, entry):
ageing_filters = frappe._dict(
{
"company": doc.company,
"report_date": doc.to_date,
"report_date": doc.posting_date,
"ageing_based_on": doc.ageing_based_on,
"range1": 30,
"range2": 60,

View File

@@ -340,10 +340,11 @@
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">30 Days</th>
<th style="width: 25%">60 Days</th>
<th style="width: 25%">90 Days</th>
<th style="width: 25%">120 Days</th>
<th style="width: 25%">0 - 30 Days</th>
<th style="width: 25%">30 - 60 Days</th>
<th style="width: 25%">60 - 90 Days</th>
<th style="width: 25%">90 - 120 Days</th>
<th style="width: 20%">Above 120 Days</th>
</tr>
</thead>
<tbody>
@@ -352,6 +353,7 @@
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
</tr>
</tbody>
</table>

View File

@@ -11,13 +11,15 @@
{
"fieldname": "cost_center_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Cost Center",
"options": "Cost Center"
"options": "Cost Center",
"reqd": 1
}
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:23.244686",
"modified": "2024-05-03 17:16:51.666461",
"modified_by": "Administrator",
"module": "Accounts",
"name": "PSOA Cost Center",

View File

@@ -15,7 +15,7 @@ class PSOACostCenter(Document):
if TYPE_CHECKING:
from frappe.types import DF
cost_center_name: DF.Link | None
cost_center_name: DF.Link
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -1063,7 +1063,7 @@ class PurchaseInvoice(BuyingController):
)
# check if the exchange rate has changed
if item.get("purchase_receipt"):
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
if (
exchange_rate_map[item.purchase_receipt]
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]

View File

@@ -2042,7 +2042,7 @@
{
"fieldname": "contact_and_address_tab",
"fieldtype": "Tab Break",
"label": "Contact & Address"
"label": "Address & Contact"
},
{
"fieldname": "payments_tab",
@@ -2203,7 +2203,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-04-11 11:30:26.272441",
"modified": "2024-05-08 18:02:28.549041",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -393,6 +393,9 @@ class SalesInvoice(SellingController):
validate_account_head(item.idx, item.income_account, self.company, "Income")
def set_tax_withholding(self):
if self.get("is_opening") == "Yes":
return
tax_withholding_details = get_party_tax_withholding_details(self)
if not tax_withholding_details:

View File

@@ -1783,6 +1783,49 @@ class TestSalesInvoice(FrappeTestCase):
self.assertTrue(gle)
def test_gle_in_transaction_currency(self):
# create multi currency sales invoice with 2 items with same income account
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_submit=True,
)
# add 2nd item with same income account
si.append(
"items",
{
"item_code": "_Test Item",
"qty": 1,
"rate": 80,
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC",
},
)
si.submit()
gl_entries = frappe.db.sql(
"""select transaction_currency, transaction_exchange_rate,
debit_in_transaction_currency, credit_in_transaction_currency
from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no=%s and account = 'Sales - _TC'
order by account asc""",
si.name,
as_dict=1,
)
expected_gle = {
"transaction_currency": "USD",
"transaction_exchange_rate": 50,
"debit_in_transaction_currency": 0,
"credit_in_transaction_currency": 180,
}
for gle in gl_entries:
for field in expected_gle:
self.assertEqual(expected_gle[field], gle[field])
def test_invoice_exchange_rate(self):
si = create_sales_invoice(
customer="_Test Customer USD",

View File

@@ -112,11 +112,7 @@ class Subscription(Document):
"""
_current_invoice_start = None
if (
self.is_new_subscription()
and self.trial_period_end
and getdate(self.trial_period_end) > getdate(self.start_date)
):
if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
_current_invoice_start = add_days(self.trial_period_end, 1)
elif self.trial_period_start and self.is_trialling():
_current_invoice_start = self.trial_period_start
@@ -143,7 +139,7 @@ class Subscription(Document):
else:
billing_cycle_info = self.get_billing_cycle_data()
if billing_cycle_info:
if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
if getdate(self.start_date) < getdate(date):
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
# For cases where trial period is for an entire billing interval
@@ -234,14 +230,14 @@ class Subscription(Document):
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
self.status = "Past Due Date"
elif not self.has_outstanding_invoice() or self.is_new_subscription():
elif not self.has_outstanding_invoice():
self.status = "Active"
def is_trialling(self) -> bool:
"""
Returns `True` if the `Subscription` is in trial period.
"""
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
return not self.period_has_passed(self.trial_period_end)
@staticmethod
def period_has_passed(
@@ -288,14 +284,6 @@ class Subscription(Document):
def invoice_document_type(self) -> str:
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
def is_new_subscription(self) -> bool:
"""
Returns `True` if `Subscription` has never generated an invoice
"""
return self.is_new() or not frappe.db.exists(
{"doctype": self.invoice_document_type, "subscription": self.name}
)
def validate(self) -> None:
self.validate_trial_period()
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
@@ -604,7 +592,7 @@ class Subscription(Document):
return False
if self.generate_invoice_at == "Beginning of the current subscription period" and (
getdate(posting_date) == getdate(self.current_invoice_start) or self.is_new_subscription()
getdate(posting_date) == getdate(self.current_invoice_start)
):
return True
elif self.generate_invoice_at == "Days before the current subscription period" and (

View File

@@ -445,11 +445,11 @@ class TestSubscription(FrappeTestCase):
# Process subscription and create first invoice
# Subscription status will be unpaid since due date has already passed
subscription.process()
subscription.process(posting_date="2018-01-01")
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.status, "Unpaid")
subscription.process()
subscription.process(posting_date="2018-04-01")
self.assertEqual(len(subscription.invoices), 1)
def test_multi_currency_subscription(self):
@@ -462,7 +462,7 @@ class TestSubscription(FrappeTestCase):
party=party,
)
subscription.process()
subscription.process(posting_date="2018-01-01")
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.status, "Unpaid")

View File

@@ -12,6 +12,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Company",
"options": "Company",
@@ -20,6 +21,7 @@
{
"fieldname": "account",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Account",
"options": "Account",
@@ -28,7 +30,7 @@
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:52.419915",
"modified": "2024-04-30 10:26:48.218294",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Account",

View File

@@ -9,6 +9,8 @@ from frappe.query_builder import Criterion
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import cint, flt, getdate
from erpnext.controllers.accounts_controller import validate_account_head
class TaxWithholdingCategory(Document):
# begin: auto-generated types
@@ -53,6 +55,7 @@ class TaxWithholdingCategory(Document):
if d.get("account") in existing_accounts:
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
validate_account_head(d.idx, d.get("account"), d.get("company"))
existing_accounts.append(d.get("account"))
def validate_thresholds(self):
@@ -282,6 +285,14 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
if taxable_vouchers:
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
# If advance is outside the current tax withholding period (usually a fiscal year), `get_deducted_tax` won't fetch it.
# updating `tax_deducted` with correct advance tax value (from current and previous previous withholding periods), will allow the
# rest of the below logic to function properly
# ---FY 2023-------------||---------------------FY 2024-----------------------||--
# ---Advance-------------||---------Inv_1--------Inv_2------------------------||--
if tax_deducted_on_advances:
tax_deducted += get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details)
tax_amount = 0
if party_type == "Supplier":
@@ -418,7 +429,7 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details):
frappe.qb.from_(at)
.inner_join(pe)
.on(pe.name == at.parent)
.select(at.parent, at.name, at.tax_amount, at.allocated_amount)
.select(pe.posting_date, at.parent, at.name, at.tax_amount, at.allocated_amount)
.where(pe.tax_withholding_category == tax_details.get("tax_withholding_category"))
.where(at.parent.isin(advances))
.where(at.account_head == tax_details.account_head)
@@ -443,6 +454,16 @@ def get_deducted_tax(taxable_vouchers, tax_details):
return sum(entries)
def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
"""
Only applies for Taxes deducted on Advance Payments
"""
advance_tax_from_across_fiscal_year = sum(
[adv.tax_amount for adv in tax_deducted_on_advances if adv.posting_date < tax_details.from_date]
)
return advance_tax_from_across_fiscal_year
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
tds_amount = 0
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}

View File

@@ -1,18 +1,22 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import datetime
import unittest
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils import today
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
test_dependencies = ["Supplier Group", "Customer Group"]
class TestTaxWithholdingCategory(unittest.TestCase):
class TestTaxWithholdingCategory(FrappeTestCase):
@classmethod
def setUpClass(self):
# create relevant supplier, etc
@@ -21,7 +25,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
make_pan_no_field()
def tearDown(self):
cancel_invoices()
frappe.db.rollback()
def test_cumulative_threshold_tds(self):
frappe.db.set_value(
@@ -317,8 +321,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
d.cancel()
def test_tds_deduction_for_po_via_payment_entry(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.db.set_value(
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
)
@@ -485,6 +487,133 @@ class TestTaxWithholdingCategory(unittest.TestCase):
pi2.cancel()
pi3.cancel()
def set_previous_fy_and_tax_category(self):
test_company = "_Test Company"
category = "Cumulative Threshold TDS"
def add_company_to_fy(fy, company):
if not [x.company for x in fy.companies if x.company == company]:
fy.append("companies", {"company": company})
fy.save()
# setup previous fiscal year
fiscal_year = get_fiscal_year(today(), company=test_company)
if prev_fiscal_year := get_fiscal_year(add_days(fiscal_year[1], -10)):
self.prev_fy = frappe.get_doc("Fiscal Year", prev_fiscal_year[0])
add_company_to_fy(self.prev_fy, test_company)
else:
# make previous fiscal year
start = datetime.date(fiscal_year[1].year - 1, fiscal_year[1].month, fiscal_year[1].day)
end = datetime.date(fiscal_year[2].year - 1, fiscal_year[2].month, fiscal_year[2].day)
self.prev_fy = frappe.get_doc(
{
"doctype": "Fiscal Year",
"year_start_date": start,
"year_end_date": end,
"companies": [{"company": test_company}],
}
)
self.prev_fy.save()
# setup tax withholding category for previous fiscal year
cat = frappe.get_doc("Tax Withholding Category", category)
cat.append(
"rates",
{
"from_date": self.prev_fy.year_start_date,
"to_date": self.prev_fy.year_end_date,
"tax_withholding_rate": 10,
"single_threshold": 0,
"cumulative_threshold": 30000,
},
)
cat.save()
def test_tds_across_fiscal_year(self):
"""
Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year
--||-----FY 2023-----||-----FY 2024-----||--
--||-----Advance-----||---Inv1---Inv2---||--
"""
self.set_previous_fy_and_tax_category()
supplier = "Test TDS Supplier"
# Cumulative threshold 30000 and tax rate 10%
category = "Cumulative Threshold TDS"
frappe.db.set_value(
"Supplier",
supplier,
{
"tax_withholding_category": category,
"pan": "ABCTY1234D",
},
)
po_and_advance_posting_date = add_days(self.prev_fy.year_end_date, -10)
po = create_purchase_order(supplier=supplier, qty=10, rate=10000)
po.transaction_date = po_and_advance_posting_date
po.taxes = []
po.apply_tds = False
po.tax_withholding_category = None
po.save().submit()
# Partial advance
payment = get_payment_entry(po.doctype, po.name)
payment.posting_date = po_and_advance_posting_date
payment.paid_amount = 60000
payment.apply_tax_withholding_amount = 1
payment.tax_withholding_category = category
payment.references = []
payment.taxes = []
payment.save().submit()
self.assertEqual(len(payment.taxes), 1)
self.assertEqual(payment.taxes[0].tax_amount, 6000)
# Multiple partial invoices
payment.reload()
pi1 = make_purchase_invoice(source_name=po.name)
pi1.apply_tds = True
pi1.tax_withholding_category = category
pi1.items[0].qty = 3
pi1.items[0].rate = 10000
advances = pi1.get_advance_entries()
pi1.append(
"advances",
{
"reference_type": advances[0].reference_type,
"reference_name": advances[0].reference_name,
"advance_amount": advances[0].amount,
"allocated_amount": 30000,
},
)
pi1.save().submit()
pi1.reload()
payment.reload()
self.assertEqual(pi1.taxes, [])
self.assertEqual(payment.taxes[0].tax_amount, 6000)
self.assertEqual(payment.taxes[0].allocated_amount, 3000)
pi2 = make_purchase_invoice(source_name=po.name)
pi2.apply_tds = True
pi2.tax_withholding_category = category
pi2.items[0].qty = 3
pi2.items[0].rate = 10000
advances = pi2.get_advance_entries()
pi2.append(
"advances",
{
"reference_type": advances[0].reference_type,
"reference_name": advances[0].reference_name,
"advance_amount": advances[0].amount,
"allocated_amount": 30000,
},
)
pi2.save().submit()
pi2.reload()
payment.reload()
self.assertEqual(pi2.taxes, [])
self.assertEqual(payment.taxes[0].tax_amount, 6000)
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
def cancel_invoices():
purchase_invoices = frappe.get_all(

View File

@@ -241,10 +241,16 @@ def merge_similar_entries(gl_map, precision=None):
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
entry.debit_in_account_currency
)
same_head.debit_in_transaction_currency = flt(same_head.debit_in_transaction_currency) + flt(
entry.debit_in_transaction_currency
)
same_head.credit = flt(same_head.credit) + flt(entry.credit)
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
entry.credit_in_account_currency
)
same_head.credit_in_transaction_currency = flt(same_head.credit_in_transaction_currency) + flt(
entry.credit_in_transaction_currency
)
else:
merged_gl_map.append(entry)

View File

@@ -188,7 +188,9 @@ def set_address_details(
*,
ignore_permissions=False,
):
billing_address_field = "customer_address" if party_type == "Lead" else party_type.lower() + "_address"
billing_address_field = (
"customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address"
)
party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
if doctype:
party_details.update(

View File

@@ -501,8 +501,9 @@ class ReceivablePayableReport:
# Deduct that from paid amount pre allocation
row.paid -= flt(payment_terms_details[0].total_advance)
# If no or single payment terms, no need to split the row
if len(payment_terms_details) <= 1:
# If single payment terms, no need to split the row
if len(payment_terms_details) == 1 and payment_terms_details[0].payment_term:
self.append_payment_term(row, payment_terms_details[0], original_row)
return
for d in payment_terms_details:

View File

@@ -266,6 +266,7 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
filters.end_date = fiscal_year.year_end_date
for company in companies:
filters.company = company
amount = get_account_type_based_gl_data(company, filters)
if amount and account_type == "Depreciation":

View File

@@ -58,9 +58,9 @@ class Deferred_Item:
For a given GL/Journal posting, get balance based on item type
"""
if self.type == "Deferred Sale Item":
return entry.debit - entry.credit
return flt(entry.debit) - flt(entry.credit)
elif self.type == "Deferred Purchase Item":
return -(entry.credit - entry.debit)
return -(flt(entry.credit) - flt(entry.debit))
return 0
def get_item_total(self):
@@ -147,7 +147,7 @@ class Deferred_Item:
actual = 0
for posting in self.gle_entries:
# if period.from_date <= posting.posting_date <= period.to_date:
if period.from_date <= posting.gle_posting_date <= period.to_date:
if period.from_date <= getdate(posting.gle_posting_date) <= period.to_date:
period_sum += self.get_amount(posting)
if posting.posted == "posted":
actual += self.get_amount(posting)
@@ -285,7 +285,7 @@ class Deferred_Revenue_and_Expense_Report:
qb.from_(inv_item)
.join(inv)
.on(inv.name == inv_item.parent)
.join(gle)
.left_join(gle)
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
.select(
inv.name.as_("doc"),

View File

@@ -279,3 +279,79 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
{"key": "aug_2021", "total": 0, "actual": 0},
]
self.assertEqual(report.period_total, expected)
@change_settings(
"Accounts Settings",
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
)
def test_zero_amount(self):
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
item = frappe.get_doc("Item", self.item)
item.enable_deferred_expense = 1
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
item.no_of_months_exp = 12
item.save()
pi = make_purchase_invoice(
item=self.item,
company=self.company,
supplier=self.supplier,
is_return=False,
update_stock=False,
posting_date=frappe.utils.datetime.date(2021, 12, 30),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
do_not_save=True,
rate=3910,
price_list_rate=3910,
warehouse=self.warehouse,
qty=1,
)
pi.set_posting_time = True
pi.items[0].enable_deferred_expense = 1
pi.items[0].service_start_date = "2021-12-30"
pi.items[0].service_end_date = "2022-12-30"
pi.items[0].deferred_expense_account = self.deferred_expense_account
pi.items[0].expense_account = self.expense_account
pi.save()
pi.submit()
pda = frappe.get_doc(
doctype="Process Deferred Accounting",
posting_date=nowdate(),
start_date="2022-01-01",
end_date="2022-01-31",
type="Expense",
company=self.company,
)
pda.insert()
pda.submit()
# execute report
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2022-01-31"))
self.filters = frappe._dict(
{
"company": self.company,
"filter_based_on": "Date Range",
"period_start_date": "2022-01-01",
"period_end_date": "2022-01-31",
"from_fiscal_year": fiscal_year.year,
"to_fiscal_year": fiscal_year.year,
"periodicity": "Monthly",
"type": "Expense",
"with_upcoming_postings": False,
}
)
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
report.run()
# fetch the invoice from deferred invoices list
inv = [d for d in report.deferred_invoices if d.name == pi.name]
# make sure the list isn't empty
self.assertTrue(inv)
# calculate the total deferred expense for the period
inv = inv[0].calculate_invoice_revenue_expense_for_period()
deferred_exp = sum([inv[idx].actual for idx in range(len(report.period_list))])
# make sure the total deferred expense is greater than 0
self.assertLess(deferred_exp, 0)

View File

@@ -460,7 +460,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
for gle in gl_entries:
group_by_value = gle.get(group_by)
gle.voucher_type = gle.voucher_type
gle.voucher_subtype = _(gle.voucher_subtype)
gle.against_voucher_type = _(gle.against_voucher_type)
gle.remarks = _(gle.remarks)

View File

@@ -720,20 +720,22 @@ class GrossProfitGenerator:
frappe.qb.from_(purchase_invoice_item)
.inner_join(purchase_invoice)
.on(purchase_invoice.name == purchase_invoice_item.parent)
.select(purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor)
.select(
purchase_invoice.name,
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
)
.where(purchase_invoice.docstatus == 1)
.where(purchase_invoice.posting_date <= self.filters.to_date)
.where(purchase_invoice_item.item_code == item_code)
)
if row.project:
query.where(purchase_invoice_item.project == row.project)
query = query.where(purchase_invoice_item.project == row.project)
if row.cost_center:
query.where(purchase_invoice_item.cost_center == row.cost_center)
query = query.where(purchase_invoice_item.cost_center == row.cost_center)
query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
query.limit(1)
query = query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc).limit(1)
last_purchase_rate = query.run()
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0

View File

@@ -516,6 +516,10 @@ def reconcile_against_document(
doc.make_advance_gl_entries()
else:
gl_map = doc.build_gl_map()
# Make sure there is no overallocation
from erpnext.accounts.general_ledger import process_debit_credit_difference
process_debit_credit_difference(gl_map)
create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
# Only update outstanding for newly linked vouchers

View File

@@ -1738,12 +1738,12 @@ def create_asset(**args):
return asset
def create_asset_category():
def create_asset_category(enable_cwip=1):
asset_category = frappe.new_doc("Asset Category")
asset_category.asset_category_name = "Computers"
asset_category.total_number_of_depreciations = 3
asset_category.frequency_of_depreciation = 3
asset_category.enable_cwip_accounting = 1
asset_category.enable_cwip_accounting = enable_cwip
asset_category.append(
"accounts",
{

View File

@@ -772,12 +772,7 @@ class TestPurchaseOrder(FrappeTestCase):
}
).insert()
else:
account = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": company},
fieldname="name",
pluck=True,
)
account = frappe.get_doc("Account", {"account_name": account_name, "company": company})
return account
@@ -808,22 +803,6 @@ class TestPurchaseOrder(FrappeTestCase):
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
pi = make_purchase_invoice(po_doc.name)
pi.append(
"advances",
{
"reference_type": pe.doctype,
"reference_name": pe.name,
"reference_row": pe.references[0].name,
"advance_amount": 5000,
"allocated_amount": 5000,
},
)
pi.save().submit()
pe.reload()
po_doc.reload()
self.assertEqual(po_doc.advance_paid, 0)
company_doc.book_advance_payments_in_separate_party_account = False
company_doc.save()

View File

@@ -406,7 +406,7 @@
{
"fieldname": "contact_and_address_tab",
"fieldtype": "Tab Break",
"label": "Contact & Address"
"label": "Address & Contact"
},
{
"fieldname": "accounting_tab",
@@ -485,7 +485,7 @@
"link_fieldname": "party"
}
],
"modified": "2024-03-27 13:10:48.412732",
"modified": "2024-05-08 18:02:57.342931",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -778,6 +778,9 @@ class AccountsController(TransactionBase):
# reset pricing rule fields if pricing_rule_removed
item.set(fieldname, value)
elif fieldname == "expense_account" and not item.get("expense_account"):
item.expense_account = value
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
"is_fixed_asset"
):

View File

@@ -712,6 +712,7 @@ class BuyingController(SubcontractingController):
def auto_make_assets(self, asset_items):
items_data = get_asset_item_details(asset_items)
messages = []
alert = False
for d in self.items:
if d.is_fixed_asset:
@@ -761,9 +762,10 @@ class BuyingController(SubcontractingController):
frappe.bold(d.item_code)
)
)
alert = True
for message in messages:
frappe.msgprint(message, title="Success", indicator="green")
frappe.msgprint(message, title="Success", indicator="green", alert=alert)
def make_asset(self, row, is_grouped_asset=False):
if not row.asset_location:

View File

@@ -1227,8 +1227,8 @@ def get_accounting_ledger_preview(doc, filters):
"debit",
"credit",
"against",
"party",
"party_type",
"party",
"cost_center",
"against_voucher_type",
"against_voucher",
@@ -1404,7 +1404,12 @@ def is_reposting_pending():
)
def future_sle_exists(args, sl_entries=None):
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
if allow_force_reposting and frappe.db.get_single_value(
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
):
return True
key = (args.voucher_type, args.voucher_no)
if not hasattr(frappe.local, "future_sle"):
frappe.local.future_sle = {}

View File

@@ -121,7 +121,7 @@ def send_mail(entry, email_campaign):
doctype="Email Campaign",
name=email_campaign.name,
subject=frappe.render_template(email_template.get("subject"), context),
content=frappe.render_template(email_template.get("response"), context),
content=frappe.render_template(email_template.response_, context),
sender=sender,
recipients=recipient_list,
communication_medium="Email",

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

@@ -93,4 +93,11 @@ frappe.query_reports["Exponential Smoothing Forecasting"] = {
},
},
],
formatter: function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname === "item_code" && value.includes("Total Quantity")) {
value = "<strong>" + value + "</strong>";
}
return value;
},
};

View File

@@ -144,7 +144,7 @@ class ForecastingReport(ExponentialSmoothingForecast):
if not self.data:
return
total_row = {"item_code": _(frappe.bold("Total Quantity"))}
total_row = {"item_code": _("Total Quantity")}
for value in self.data:
for period in self.period_list:

View File

@@ -363,4 +363,5 @@ erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency

View File

@@ -0,0 +1,21 @@
import frappe
def execute():
# update debit and credit in transaction currency:
# if transaction currency is same as account currency,
# then debit and credit in transaction currency is same as debit and credit in account currency
# else debit and credit divided by exchange rate
# nosemgrep
frappe.db.sql(
"""
UPDATE `tabGL Entry`
SET
debit_in_transaction_currency = IF(transaction_currency = account_currency, debit_in_account_currency, debit / transaction_exchange_rate),
credit_in_transaction_currency = IF(transaction_currency = account_currency, credit_in_account_currency, credit / transaction_exchange_rate)
WHERE
transaction_exchange_rate > 0
and transaction_currency is not null
"""
)

View File

@@ -22,6 +22,7 @@ erpnext.accounts.dimensions = {
});
me.default_dimensions = r.message[1];
me.setup_filters(frm, doctype);
me.update_dimension(frm, doctype);
},
});
},

View File

@@ -373,6 +373,7 @@ erpnext.sales_common = {
frappe.model.set_value(item.doctype, item.name, {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
incoming_rate: r.avg_rate,
qty:
qty /
flt(

View File

@@ -482,7 +482,7 @@
{
"fieldname": "contact_and_address_tab",
"fieldtype": "Tab Break",
"label": "Contact & Address"
"label": "Address & Contact"
},
{
"fieldname": "defaults_tab",
@@ -583,7 +583,7 @@
"link_fieldname": "party"
}
],
"modified": "2024-03-27 13:06:48.056107",
"modified": "2024-05-08 18:03:20.716169",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -71,6 +71,8 @@ frappe.ui.form.on("Quotation", {
frm.trigger("set_label");
frm.trigger("toggle_reqd_lead_customer");
frm.trigger("set_dynamic_field_label");
frm.set_value("party_name", "");
frm.set_value("customer_name", "");
},
set_label: function (frm) {
@@ -97,7 +99,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
frappe.dynamic_link = {
doc: this.frm.doc,
fieldname: "party_name",
doctype: doc.quotation_to == "Customer" ? "Customer" : "Lead",
doctype: doc.quotation_to,
};
var me = this;
@@ -197,6 +199,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
};
} else if (this.frm.doc.quotation_to == "Prospect") {
this.frm.set_df_property("party_name", "label", "Prospect");
this.frm.fields_dict.party_name.get_query = null;
}
}

View File

@@ -238,7 +238,7 @@ def update_qty(bin_name, args):
sle = frappe.qb.DocType("Stock Ledger Entry")
# actual qty is not up to date in case of backdated transaction
if future_sle_exists(args):
if future_sle_exists(args, allow_force_reposting=False):
last_sle_qty = (
frappe.qb.from_(sle)
.select(sle.qty_after_transaction)

View File

@@ -124,13 +124,14 @@
"per_returned",
"transporter_info",
"transporter",
"driver",
"lr_no",
"vehicle_no",
"delivery_trip",
"driver",
"col_break34",
"transporter_name",
"driver_name",
"lr_date",
"vehicle_no",
"driver_name",
"customer_po_details",
"po_no",
"column_break_17",
@@ -1391,13 +1392,20 @@
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
},
{
"fieldname": "delivery_trip",
"fieldtype": "Link",
"label": "Delivery Trip",
"options": "Delivery Trip",
"print_hide": 1
}
],
"icon": "fa fa-truck",
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:06:49.519676",
"modified": "2024-03-29 19:03:55.374173",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -64,6 +64,7 @@ class DeliveryNote(SellingController):
customer_address: DF.Link | None
customer_group: DF.Link | None
customer_name: DF.Data | None
delivery_trip: DF.Link | None
disable_rounded_total: DF.Check
discount_amount: DF.Currency
dispatch_address: DF.TextEditor | None
@@ -76,7 +77,7 @@ class DeliveryNote(SellingController):
ignore_pricing_rule: DF.Check
in_words: DF.Data | None
incoterm: DF.Link | None
installation_status: DF.Literal[None]
installation_status: DF.LiteralNone
instructions: DF.Text | None
inter_company_reference: DF.Link | None
is_internal_customer: DF.Check
@@ -1066,7 +1067,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
@frappe.whitelist()
def make_delivery_trip(source_name, target_doc=None):
def make_delivery_trip(source_name, target_doc=None, kwargs=None):
if not target_doc:
target_doc = frappe.new_doc("Delivery Trip")
doclist = get_mapped_doc(
@@ -1075,7 +1076,6 @@ def make_delivery_trip(source_name, target_doc=None):
{
"Delivery Note": {
"doctype": "Delivery Stop",
"validation": {"docstatus": ["=", 1]},
"on_parent": target_doc,
"field_map": {
"name": "delivery_note",

View File

@@ -30,12 +30,6 @@ frappe.listview_settings["Delivery Note"] = {
const docnames = doclist.get_checked_items(true);
if (selected_docs.length > 0) {
for (let doc of selected_docs) {
if (!doc.docstatus) {
frappe.throw(__("Cannot create a Delivery Trip from Draft documents."));
}
}
frappe.new_doc("Delivery Trip").then(() => {
// Empty out the child table before inserting new ones
cur_frm.set_value("delivery_stops", []);

View File

@@ -22,6 +22,7 @@ from erpnext.stock.doctype.delivery_note.delivery_note import (
make_delivery_trip,
make_sales_invoice,
)
from erpnext.stock.doctype.delivery_trip.test_delivery_trip import create_driver
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
@@ -1064,6 +1065,21 @@ class TestDeliveryNote(FrappeTestCase):
dn = create_delivery_note()
dt = make_delivery_trip(dn.name)
self.assertEqual(dn.name, dt.delivery_stops[0].delivery_note)
dt.delivery_stops[0].customer_address = "fake string"
dt.flags.ignore_mandatory = True
dt.save()
dn.reload()
self.assertEqual(dn.delivery_trip, dt.name)
dn = create_delivery_note(do_not_submit=True)
dt = make_delivery_trip(dn.name)
self.assertEqual(dn.name, dt.delivery_stops[0].delivery_note)
dt.driver = create_driver()
self.assertRaisesRegex(
frappe.exceptions.ValidationError,
r"^Delivery Notes should not be in draft state when submitting a Delivery Trip.*",
dt.submit,
)
def test_delivery_note_with_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center

View File

@@ -60,7 +60,6 @@ frappe.ui.form.on("Delivery Trip", {
company: frm.doc.company,
},
get_query_filters: {
docstatus: 1,
company: frm.doc.company,
},
});

View File

@@ -54,11 +54,18 @@ class DeliveryTrip(Document):
if self._action == "submit" and not self.driver:
frappe.throw(_("A driver must be set to submit."))
if self._action == "submit":
self.validate_delivery_note_not_draft()
self.validate_stop_addresses()
def on_update(self):
self.update_delivery_notes()
def on_trash(self):
self.update_delivery_notes(delete=True)
def on_submit(self):
self.update_status()
self.update_delivery_notes()
def on_update_after_submit(self):
self.update_status()
@@ -72,6 +79,20 @@ class DeliveryTrip(Document):
if not stop.customer_address:
stop.customer_address = get_address_display(frappe.get_doc("Address", stop.address).as_dict())
def validate_delivery_note_not_draft(self):
delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note))
draft_delivery_notes = frappe.get_all(
"Delivery Note",
{"docstatus": 0, "name": ["in", delivery_notes]},
pluck="name",
)
if draft_delivery_notes:
frappe.throw(
_(
"Delivery Notes should not be in draft state when submitting a Delivery Trip. The following Delivery Notes are still in draft state: {0}. Please submit them first."
).format(", ".join(draft_delivery_notes))
)
def update_status(self):
status = {0: "Draft", 1: "Scheduled", 2: "Cancelled"}[self.docstatus]
@@ -100,22 +121,29 @@ class DeliveryTrip(Document):
"driver": self.driver,
"driver_name": self.driver_name,
"vehicle_no": self.vehicle,
"delivery_trip": self.name,
"lr_no": self.name,
"lr_date": self.departure_time,
}
delivery_notes_updated = set()
for delivery_note in delivery_notes:
note_doc = frappe.get_doc("Delivery Note", delivery_note)
for field, value in update_fields.items():
prev_value = getattr(note_doc, field)
value = None if delete else value
if prev_value != value:
delivery_notes_updated.add(delivery_note)
setattr(note_doc, field, value)
note_doc.flags.ignore_validate_update_after_submit = True
note_doc.save()
if delivery_note in delivery_notes_updated:
note_doc.flags.ignore_validate_update_after_submit = True
note_doc.save()
delivery_notes = [get_link_to_form("Delivery Note", note) for note in delivery_notes]
frappe.msgprint(_("Delivery Notes {0} updated").format(", ".join(delivery_notes)))
delivery_notes_updated = [get_link_to_form("Delivery Note", note) for note in delivery_notes_updated]
frappe.msgprint(_("Delivery Notes {0} updated").format(", ".join(delivery_notes_updated)))
@frappe.whitelist()
def process_route(self, optimize):

View File

@@ -15,6 +15,9 @@ frappe.ui.form.on("Item", {
frm.add_fetch("tax_type", "tax_rate", "tax_rate");
frm.make_methods = {
Quotation: () => {
open_form(frm, "Quotation", "Quotation Item", "items");
},
"Sales Order": () => {
open_form(frm, "Sales Order", "Sales Order Item", "items");
},

View File

@@ -36,6 +36,8 @@
"section_break_11",
"description",
"brand",
"unit_of_measure_conversion",
"uoms",
"dashboard_tab",
"inventory_section",
"inventory_settings_section",
@@ -52,8 +54,6 @@
"barcodes",
"reorder_section",
"reorder_levels",
"unit_of_measure_conversion",
"uoms",
"serial_nos_and_batches",
"has_batch_no",
"create_new_batch",
@@ -891,7 +891,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2024-03-27 13:09:53.380997",
"modified": "2024-04-30 13:46:39.098753",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -65,15 +65,13 @@ class Item(Document):
from erpnext.stock.doctype.item_reorder.item_reorder import ItemReorder
from erpnext.stock.doctype.item_supplier.item_supplier import ItemSupplier
from erpnext.stock.doctype.item_tax.item_tax import ItemTax
from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import (
ItemVariantAttribute,
)
from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import ItemVariantAttribute
from erpnext.stock.doctype.uom_conversion_detail.uom_conversion_detail import UOMConversionDetail
allow_alternative_item: DF.Check
allow_negative_stock: DF.Check
asset_category: DF.Link | None
asset_naming_series: DF.Literal
asset_naming_series: DF.Literal[None]
attributes: DF.Table[ItemVariantAttribute]
auto_create_assets: DF.Check
barcodes: DF.Table[ItemBarcode]

View File

@@ -156,6 +156,33 @@ class TestItem(FrappeTestCase):
for key, value in to_check.items():
self.assertEqual(value, details.get(key), key)
def test_get_asset_item_details(self):
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
create_asset_category(0)
create_fixed_asset_item()
details = get_item_details(
{
"item_code": "Macbook Pro",
"company": "_Test Company",
"currency": "INR",
"doctype": "Purchase Receipt",
}
)
self.assertEqual(details.get("expense_account"), "_Test Fixed Asset - _TC")
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", "1")
details = get_item_details(
{
"item_code": "Macbook Pro",
"company": "_Test Company",
"currency": "INR",
"doctype": "Purchase Receipt",
}
)
self.assertEqual(details.get("expense_account"), "CWIP Account - _TC")
def test_item_tax_template(self):
expected_item_tax_template = [
{

View File

@@ -52,10 +52,13 @@
"search_index": 1
},
{
"fetch_from": "item_code.stock_uom",
"fetch_if_empty": 1,
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM"
"options": "UOM",
"reqd": 1
},
{
"default": "0",
@@ -221,7 +224,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-27 13:09:54.930834",
"modified": "2024-04-02 22:18:00.450641",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Price",

View File

@@ -38,7 +38,7 @@ class ItemPrice(Document):
reference: DF.Data | None
selling: DF.Check
supplier: DF.Link | None
uom: DF.Link | None
uom: DF.Link
valid_from: DF.Date | None
valid_upto: DF.Date | None
# end: auto-generated types

View File

@@ -790,7 +790,7 @@ def get_available_item_locations(
locations = get_locations_based_on_required_qty(locations, required_qty)
if not ignore_validation:
validate_picked_materials(item_code, required_qty, locations)
validate_picked_materials(item_code, required_qty, locations, picked_item_details)
return locations
@@ -810,7 +810,7 @@ def get_locations_based_on_required_qty(locations, required_qty):
return filtered_locations
def validate_picked_materials(item_code, required_qty, locations):
def validate_picked_materials(item_code, required_qty, locations, picked_item_details=None):
for location in list(locations):
if location["qty"] < 0:
locations.remove(location)
@@ -819,15 +819,25 @@ def validate_picked_materials(item_code, required_qty, locations):
remaining_qty = required_qty - total_qty_available
if remaining_qty > 0:
frappe.msgprint(
_("{0} units of Item {1} is picked in another Pick List.").format(
remaining_qty, get_link_to_form("Item", item_code)
),
title=_("Already Picked"),
)
if picked_item_details:
frappe.msgprint(
_("{0} units of Item {1} is picked in another Pick List.").format(
remaining_qty, get_link_to_form("Item", item_code)
),
title=_("Already Picked"),
)
else:
frappe.msgprint(
_("{0} units of Item {1} is not available in any of the warehouses.").format(
remaining_qty, get_link_to_form("Item", item_code)
),
title=_("Insufficient Stock"),
)
def filter_locations_by_picked_materials(locations, picked_item_details) -> list[dict]:
filterd_locations = []
for row in locations:
key = row.warehouse
if row.batch_no:
@@ -835,6 +845,7 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list
picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0)
if not picked_qty:
filterd_locations.append(row)
continue
if picked_qty > row.qty:
row.qty = 0
@@ -845,7 +856,10 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list
if row.serial_nos:
row.serial_nos = list(set(row.serial_nos) - set(picked_item_details[key].get("serial_no")))
return locations
if row.qty > 0:
filterd_locations.append(row)
return filterd_locations
def get_available_item_locations_for_serial_and_batched_item(

View File

@@ -977,3 +977,157 @@ class TestPickList(FrappeTestCase):
so = make_sales_order(item_code=item, qty=4, rate=100)
pl = create_pick_list(so.name)
self.assertFalse(hasattr(pl, "locations"))
def test_pick_list_validation_for_multiple_batches_and_sales_order(self):
warehouse = "_Test Warehouse - _TC"
item = make_item(
"Test Batch Pick List Item For Multiple Batches",
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "SN-BT-BATCH-SPLIMBATCH-.####",
"create_new_batch": 1,
},
).name
make_stock_entry(item=item, to_warehouse=warehouse, qty=5)
make_stock_entry(item=item, to_warehouse=warehouse, qty=5)
so = make_sales_order(item_code=item, qty=6, rate=100)
pl1 = create_pick_list(so.name)
pl1.save()
self.assertEqual(pl1.locations[0].qty, 5.0)
self.assertEqual(pl1.locations[1].qty, 1.0)
so = make_sales_order(item_code=item, qty=4, rate=100)
pl = create_pick_list(so.name)
pl.save()
self.assertEqual(pl.locations[0].qty, 4.0)
self.assertTrue(hasattr(pl, "locations"))
pl1.submit()
pl.reload()
pl.submit()
self.assertEqual(pl.locations[0].qty, 4.0)
self.assertTrue(hasattr(pl, "locations"))
def test_pick_list_for_multiple_sales_order_with_multiple_batches(self):
warehouse = "_Test Warehouse - _TC"
item = make_item(
"Test Batch Pick List Item For Multiple Batches and Sales Order",
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "SN-SOO-BT-SPLIMBATCH-.####",
"create_new_batch": 1,
},
).name
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
so = make_sales_order(item_code=item, qty=10, rate=100)
pl1 = create_pick_list(so.name)
pl1.save()
self.assertEqual(pl1.locations[0].qty, 10)
so = make_sales_order(item_code=item, qty=110, rate=100)
pl = create_pick_list(so.name)
pl.save()
self.assertEqual(pl.locations[0].qty, 90.0)
self.assertEqual(pl.locations[1].qty, 20.0)
self.assertTrue(hasattr(pl, "locations"))
pl1.submit()
pl.reload()
pl.submit()
self.assertEqual(pl.locations[0].qty, 90.0)
self.assertEqual(pl.locations[1].qty, 20.0)
self.assertTrue(hasattr(pl, "locations"))
def test_pick_list_for_multiple_sales_order_with_multiple_serial_nos(self):
warehouse = "_Test Warehouse - _TC"
item = make_item(
"Test Serial No Pick List Item For Multiple Batches and Sales Order",
properties={
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "SNNN-SOO-BT-SPLIMBATCH-.####",
},
).name
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
so = make_sales_order(item_code=item, qty=10, rate=100)
pl1 = create_pick_list(so.name)
pl1.save()
self.assertEqual(pl1.locations[0].qty, 10)
serial_nos = pl1.locations[0].serial_no.split("\n")
self.assertEqual(len(serial_nos), 10)
so = make_sales_order(item_code=item, qty=110, rate=100)
pl = create_pick_list(so.name)
pl.save()
self.assertEqual(pl.locations[0].qty, 110.0)
self.assertTrue(hasattr(pl, "locations"))
new_serial_nos = pl.locations[0].serial_no.split("\n")
self.assertEqual(len(new_serial_nos), 110)
for sn in serial_nos:
self.assertFalse(sn in new_serial_nos)
pl1.submit()
pl.reload()
pl.submit()
self.assertEqual(pl.locations[0].qty, 110.0)
self.assertTrue(hasattr(pl, "locations"))
def test_pick_list_for_multiple_sales_orders_for_non_serialized_item(self):
warehouse = "_Test Warehouse - _TC"
item = make_item(
"Test Non Serialized Pick List Item For Multiple Batches and Sales Order",
properties={
"is_stock_item": 1,
},
).name
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
so = make_sales_order(item_code=item, qty=10, rate=100)
pl1 = create_pick_list(so.name)
pl1.save()
self.assertEqual(pl1.locations[0].qty, 10)
so = make_sales_order(item_code=item, qty=110, rate=100)
pl = create_pick_list(so.name)
pl.save()
self.assertEqual(pl.locations[0].qty, 110.0)
self.assertTrue(hasattr(pl, "locations"))
pl1.submit()
pl.reload()
pl.submit()
self.assertEqual(pl.locations[0].qty, 110.0)
self.assertTrue(hasattr(pl, "locations"))
so = make_sales_order(item_code=item, qty=110, rate=100)
pl = create_pick_list(so.name)
pl.save()
self.assertEqual(pl.locations[0].qty, 80.0)

View File

@@ -132,7 +132,8 @@
"in_list_view": 1,
"label": "Item",
"options": "Item",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fieldname": "quantity_section",
@@ -240,7 +241,7 @@
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:13.391216",
"modified": "2024-05-07 15:32:42.905446",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",

View File

@@ -671,19 +671,8 @@ class PurchaseReceipt(BuyingController):
else self.get_company_default("stock_received_but_not_billed")
)
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
if d.is_fixed_asset:
if is_cwip_accounting_enabled(d.asset_category):
stock_asset_account_name = get_asset_account(
"capital_work_in_progress_account",
asset_category=d.asset_category,
company=self.company,
)
else:
stock_asset_account_name = get_asset_category_account(
"fixed_asset_account", asset_category=d.asset_category, company=self.company
)
stock_asset_account_name = d.expense_account
stock_value_diff = (
flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount)
)

View File

@@ -1149,7 +1149,18 @@ def make_batch_nos(item_code, batch_nos):
continue
batch_nos_details.append(
(batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description)
(
batch_no,
batch_no,
now(),
now(),
user,
user,
item.item_code,
item.item_name,
item.description,
1,
)
)
fields = [
@@ -1162,6 +1173,7 @@ def make_batch_nos(item_code, batch_nos):
"item",
"item_name",
"description",
"use_batchwise_valuation",
]
frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details))

View File

@@ -498,6 +498,8 @@ class TestSerialandBatchBundle(FrappeTestCase):
make_batch_nos(item_code, batch_nos)
self.assertTrue(frappe.db.exists("Batch", batch_id))
use_batchwise_valuation = frappe.db.get_value("Batch", batch_id, "use_batchwise_valuation")
self.assertEqual(use_batchwise_valuation, 1)
batch_id = "TEST-BATTCCH-VAL-00001"
batch_nos = [{"batch_no": batch_id, "qty": 1}]

View File

@@ -1340,6 +1340,7 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
frappe.model.set_value(item.doctype, item.name, {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
basic_rate: r.avg_rate,
qty:
Math.abs(r.total_qty) /
flt(item.conversion_factor || 1, precision("conversion_factor", item)),

View File

@@ -13,6 +13,7 @@
"end_time",
"limits_dont_apply_on",
"item_based_reposting",
"do_reposting_for_each_stock_transaction",
"errors_notification_section",
"notify_reposting_error_to_role"
],
@@ -65,12 +66,18 @@
"fieldname": "errors_notification_section",
"fieldtype": "Section Break",
"label": "Errors Notification"
},
{
"default": "0",
"fieldname": "do_reposting_for_each_stock_transaction",
"fieldtype": "Check",
"label": "Do reposting for each Stock Transaction"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-03-27 13:10:45.069645",
"modified": "2024-04-24 12:19:40.204888",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reposting Settings",

View File

@@ -16,6 +16,7 @@ class StockRepostingSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
do_reposting_for_each_stock_transaction: DF.Check
end_time: DF.Time | None
item_based_reposting: DF.Check
limit_reposting_timeslot: DF.Check
@@ -29,6 +30,10 @@ class StockRepostingSettings(Document):
def validate(self):
self.set_minimum_reposting_time_slot()
def before_save(self):
if self.do_reposting_for_each_stock_transaction:
self.item_based_reposting = 1
def set_minimum_reposting_time_slot(self):
"""Ensure that timeslot for reposting is at least 12 hours."""
if not self.limit_reposting_timeslot:

View File

@@ -38,3 +38,51 @@ class TestStockRepostingSettings(unittest.TestCase):
users = get_recipients()
self.assertTrue(user in users)
def test_do_reposting_for_each_stock_transaction(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1)
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
item = make_item(
"_Test item for reposting check for each transaction", properties={"is_stock_item": 1}
).name
stock_entry = make_stock_entry(
item_code=item,
qty=1,
rate=100,
stock_entry_type="Material Receipt",
target="_Test Warehouse - _TC",
)
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
self.assertTrue(riv)
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
def test_do_not_reposting_for_each_stock_transaction(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0)
if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"):
frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0)
item = make_item(
"_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1}
).name
stock_entry = make_stock_entry(
item_code=item,
qty=1,
rate=100,
stock_entry_type="Material Receipt",
target="_Test Warehouse - _TC",
)
riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name")
self.assertFalse(riv)

View File

@@ -418,7 +418,7 @@
{
"default": "0",
"depends_on": "eval: doc.enable_stock_reservation",
"description": "Stock will be reserved on submission of <b>Purchase Receipt</b> created against Material Receipt for Sales Order.",
"description": "Stock will be reserved on submission of <b>Purchase Receipt</b> created against Material Request for Sales Order.",
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
"fieldtype": "Check",
"label": "Auto Reserve Stock for Sales Order on Purchase"
@@ -469,4 +469,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -74,7 +74,6 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
args["bill_date"] = doc.get("bill_date")
out = get_basic_details(args, item, overwrite_warehouse)
get_item_tax_template(args, item, out)
out["item_tax_rate"] = get_item_tax_map(
args.company,
@@ -293,12 +292,26 @@ def get_basic_details(args, item, overwrite_warehouse=True):
expense_account = None
if args.get("doctype") == "Purchase Invoice" and item.is_fixed_asset:
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
if item.is_fixed_asset:
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
expense_account = get_asset_category_account(
fieldname="fixed_asset_account", item=args.item_code, company=args.company
)
if is_cwip_accounting_enabled(item.asset_category):
expense_account = get_asset_account(
"capital_work_in_progress_account",
asset_category=item.asset_category,
company=args.company,
)
elif args.get("doctype") in (
"Purchase Invoice",
"Purchase Receipt",
"Purchase Order",
"Material Request",
):
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
expense_account = get_asset_category_account(
fieldname="fixed_asset_account", item=args.item_code, company=args.company
)
# Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master
if not args.get("uom"):
@@ -840,7 +853,12 @@ def insert_item_price(args):
item_price = frappe.db.get_value(
"Item Price",
{"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency},
{
"item_code": args.item_code,
"price_list": args.price_list,
"currency": args.currency,
"uom": args.stock_uom,
},
["name", "price_list_rate"],
as_dict=1,
)

View File

@@ -40,16 +40,25 @@ frappe.query_reports["Batch-Wise Balance History"] = {
};
},
},
{
fieldname: "warehouse_type",
label: __("Warehouse Type"),
fieldtype: "Link",
width: "80",
options: "Warehouse Type",
},
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
options: "Warehouse",
get_query: function () {
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
let company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
...(warehouse_type && { warehouse_type }),
...(company && { company }),
},
};
},

View File

@@ -30,8 +30,15 @@ def execute(filters=None):
sle_count = _estimate_table_row_count("Stock Ledger Entry")
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
if (
sle_count > SLE_COUNT_LIMIT
and not filters.get("item_code")
and not filters.get("warehouse")
and not filters.get("warehouse_type")
):
frappe.throw(
_("Please select either the Item or Warehouse or Warehouse Type filter to generate the report.")
)
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
@@ -121,6 +128,16 @@ def get_stock_ledger_entries_for_batch_no(filters):
)
query = apply_warehouse_filter(query, sle, filters)
if filters.warehouse_type and not filters.warehouse:
warehouses = frappe.get_all(
"Warehouse",
filters={"warehouse_type": filters.warehouse_type, "is_group": 0},
pluck="name",
)
if warehouses:
query = query.where(sle.warehouse.isin(warehouses))
for field in ["item_code", "batch_no", "company"]:
if filters.get(field):
query = query.where(sle[field] == filters.get(field))
@@ -154,6 +171,16 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
)
query = apply_warehouse_filter(query, sle, filters)
if filters.warehouse_type and not filters.warehouse:
warehouses = frappe.get_all(
"Warehouse",
filters={"warehouse_type": filters.warehouse_type, "is_group": 0},
pluck="name",
)
if warehouses:
query = query.where(sle.warehouse.isin(warehouses))
for field in ["item_code", "batch_no", "company"]:
if filters.get(field):
if field == "batch_no":

View File

@@ -18,15 +18,24 @@ frappe.query_reports["Stock Ageing"] = {
default: frappe.datetime.get_today(),
reqd: 1,
},
{
fieldname: "warehouse_type",
label: __("Warehouse Type"),
fieldtype: "Link",
width: "80",
options: "Warehouse Type",
},
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
options: "Warehouse",
get_query: () => {
const company = frappe.query_report.get_filter_value("company");
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
let company = frappe.query_report.get_filter_value("company");
return {
filters: {
...(warehouse_type && { warehouse_type }),
...(company && { company }),
},
};

View File

@@ -434,6 +434,15 @@ class FIFOSlots:
if self.filters.get("warehouse"):
sle_query = self.__get_warehouse_conditions(sle, sle_query)
elif self.filters.get("warehouse_type"):
warehouses = frappe.get_all(
"Warehouse",
filters={"warehouse_type": self.filters.get("warehouse_type"), "is_group": 0},
pluck="name",
)
if warehouses:
sle_query = sle_query.where(sle.warehouse.isin(warehouses))
sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty)

View File

@@ -146,6 +146,8 @@ class StockBalanceReport:
if self.filters.get("show_stock_ageing_data"):
self.sle_entries = self.sle_query.run(as_dict=True)
# HACK: This is required to avoid causing db query in flt
_system_settings = frappe.get_cached_doc("System Settings")
with frappe.db.unbuffered_cursor():
if not self.filters.get("show_stock_ageing_data"):
self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)

View File

@@ -231,13 +231,6 @@ def get_columns(filters):
"width": 100,
"convertible": "qty",
},
{
"label": _("Voucher #"),
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"options": "voucher_type",
"width": 150,
},
{
"label": _("Warehouse"),
"fieldname": "warehouse",

View File

@@ -56,13 +56,14 @@ def execute(filters=None):
item_value.setdefault((item, item_map[item]["item_group"]), [])
item_value[(item, item_map[item]["item_group"])].append(total_stock_value)
itemwise_brand = frappe._dict(get_itemwise_brand(items))
# sum bal_qty by item
for (item, item_group), wh_balance in item_balance.items():
if not item_ageing.get(item):
continue
total_stock_value = sum(item_value[(item, item_group)])
row = [item, item_map[item]["item_name"], item_group, total_stock_value]
row = [item, item_map[item]["item_name"], item_group, itemwise_brand.get(item), total_stock_value]
fifo_queue = item_ageing[item]["fifo_queue"]
average_age = 0.00
@@ -85,6 +86,10 @@ def execute(filters=None):
return columns, data
def get_itemwise_brand(items):
return frappe.get_all("Item", filters={"name": ("in", items)}, fields=["name", "brand"], as_list=1)
def get_columns(filters):
"""return columns"""
@@ -92,6 +97,7 @@ def get_columns(filters):
_("Item") + ":Link/Item:150",
_("Item Name") + ":Link/Item:150",
_("Item Group") + "::120",
_("Brand") + ":Link/Brand:120",
_("Value") + ":Currency:120",
_("Age") + ":Float:120",
]

View File

@@ -553,7 +553,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
self.set_stock_value_difference()
def get_batch_no_ledgers(self) -> list[dict]:
if not self.batchwise_valuation_batches:
if not self.batches:
return []
parent = frappe.qb.DocType("Serial and Batch Bundle")
@@ -575,7 +575,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
Sum(child.qty).as_("qty"),
)
.where(
(child.batch_no.isin(self.batchwise_valuation_batches))
(child.batch_no.isin(self.batches))
& (parent.warehouse == self.sle.warehouse)
& (parent.item_code == self.sle.item_code)
& (parent.docstatus == 1)

View File

@@ -67,3 +67,9 @@ typing-modules = ["frappe.types.DF"]
quote-style = "double"
indent-style = "tab"
docstring-code-format = true
[project.urls]
Homepage = "https://erpnext.com/"
Repository = "https://github.com/frappe/erpnext.git"
"Bug Reports" = "https://github.com/frappe/erpnext/issues"