mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-29 15:38:18 +00:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9397a57d4d | ||
|
|
d70050931b | ||
|
|
1875d69f60 | ||
|
|
b2d71b44cf | ||
|
|
b172ae0557 | ||
|
|
bf3349a432 | ||
|
|
5dd99f896e | ||
|
|
7579e00425 | ||
|
|
c22869fed9 | ||
|
|
57e2619cf1 | ||
|
|
66d0ad1bc6 | ||
|
|
3395e7c2cd | ||
|
|
0721816763 | ||
|
|
e45d0779ef | ||
|
|
c6ce76170b | ||
|
|
de3e6922b5 | ||
|
|
ae6d3f27a2 | ||
|
|
7795030b7b | ||
|
|
9bac43acff | ||
|
|
1e987153c9 | ||
|
|
d36a7c2389 | ||
|
|
b548cc411d | ||
|
|
ad3f985dc4 | ||
|
|
f3ba5a81ab | ||
|
|
1b6aeba267 | ||
|
|
e393ce9a47 | ||
|
|
8951efb457 | ||
|
|
5db2a19778 | ||
|
|
288206bdcd | ||
|
|
b3c3733286 | ||
|
|
b2b49446d4 | ||
|
|
391b5c4226 | ||
|
|
ce454d5202 | ||
|
|
f93feb18fb | ||
|
|
0793213981 | ||
|
|
4edfc6f125 | ||
|
|
98df0614ab | ||
|
|
59d0ff493f | ||
|
|
dc3b5e2f3a | ||
|
|
058a2f0c42 | ||
|
|
6c443bd85a | ||
|
|
a228d1edc5 | ||
|
|
87628835bf | ||
|
|
e69c722534 | ||
|
|
326c37a051 | ||
|
|
2a70791bba | ||
|
|
e5fb77c65f | ||
|
|
4a7d401dc5 | ||
|
|
c3221c4e93 | ||
|
|
412e6be502 | ||
|
|
4df5f18d85 | ||
|
|
d58e527b6b | ||
|
|
58eb1849d7 | ||
|
|
2ebea8866a | ||
|
|
4e65b7873d | ||
|
|
7dc23d9733 | ||
|
|
f964178008 | ||
|
|
2bfaf64fff | ||
|
|
6da00319e6 | ||
|
|
3834d6fbce |
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
30
.github/workflows/label-base-on-title.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: "Auto-label PRs based on title"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
add-label-if-prefix-matches:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR title and add label if it matches prefixes
|
||||
uses: actions/github-script@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
script: |
|
||||
const title = context.payload.pull_request.title.toLowerCase();
|
||||
const prefixes = ['chore', 'ci', 'style', 'test', 'refactor'];
|
||||
|
||||
// Check if the PR title starts with any of the prefixes
|
||||
if (prefixes.some(prefix => title.startsWith(prefix))) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: ['skip-release-notes']
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.54.5"
|
||||
__version__ = "15.55.5"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -116,6 +116,7 @@ def identify_is_group(child):
|
||||
return is_group
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_chart(chart_template, existing_company=None):
|
||||
chart = {}
|
||||
if existing_company:
|
||||
|
||||
@@ -159,9 +159,6 @@ def get_payment_entries_for_bank_clearance(
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if bank_account:
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
@@ -183,7 +180,6 @@ def get_payment_entries_for_bank_clearance(
|
||||
"account": account,
|
||||
"from": from_date,
|
||||
"to": to_date,
|
||||
"bank_account": bank_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -576,8 +576,22 @@ class JournalEntry(AccountsController):
|
||||
if customers:
|
||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||
|
||||
customer_details = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Customer Credit Limit",
|
||||
filters={
|
||||
"parent": ["in", customers],
|
||||
"parenttype": ["=", "Customer"],
|
||||
"company": ["=", self.company],
|
||||
},
|
||||
fields=["parent", "bypass_credit_limit_check"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
for customer in customers:
|
||||
check_credit_limit(customer, self.company)
|
||||
ignore_outstanding_sales_order = bool(customer_details.get(customer))
|
||||
check_credit_limit(customer, self.company, ignore_outstanding_sales_order)
|
||||
|
||||
def validate_cheque_info(self):
|
||||
if self.voucher_type in ["Bank Entry"]:
|
||||
|
||||
@@ -7,6 +7,7 @@ from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Tuple
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
@@ -37,7 +38,7 @@ from erpnext.accounts.general_ledger import (
|
||||
make_reverse_gl_entries,
|
||||
process_gl_map,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.party import complete_contact_details, get_party_account, set_contact_details
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
@@ -439,6 +440,12 @@ class PaymentEntry(AccountsController):
|
||||
self.party_name = frappe.db.get_value(self.party_type, self.party, "name")
|
||||
|
||||
if self.party:
|
||||
if not self.contact_person:
|
||||
set_contact_details(
|
||||
self, party=frappe._dict({"name": self.party}), party_type=self.party_type
|
||||
)
|
||||
else:
|
||||
complete_contact_details(self)
|
||||
if not self.party_balance:
|
||||
self.party_balance = get_balance_on(
|
||||
party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
|
||||
@@ -736,16 +743,39 @@ class PaymentEntry(AccountsController):
|
||||
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
|
||||
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
|
||||
|
||||
conversion_rate = frappe.db.get_value(key[2], {"name": key[1]}, "conversion_rate")
|
||||
base_paid_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Schedule").get_field("base_paid_amount")
|
||||
)
|
||||
base_outstanding_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Schedule").get_field("base_outstanding")
|
||||
)
|
||||
|
||||
base_paid_amount = flt(
|
||||
(allocated_amount - discounted_amt) * conversion_rate, base_paid_amount_precision
|
||||
)
|
||||
base_outstanding = flt(allocated_amount * conversion_rate, base_outstanding_precision)
|
||||
|
||||
if cancel:
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabPayment Schedule`
|
||||
SET
|
||||
paid_amount = `paid_amount` - %s,
|
||||
base_paid_amount = `base_paid_amount` - %s,
|
||||
discounted_amount = `discounted_amount` - %s,
|
||||
outstanding = `outstanding` + %s
|
||||
outstanding = `outstanding` + %s,
|
||||
base_outstanding = `base_outstanding` - %s
|
||||
WHERE parent = %s and payment_term = %s""",
|
||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
||||
(
|
||||
allocated_amount - discounted_amt,
|
||||
base_paid_amount,
|
||||
discounted_amt,
|
||||
allocated_amount,
|
||||
base_outstanding,
|
||||
key[1],
|
||||
key[0],
|
||||
),
|
||||
)
|
||||
else:
|
||||
if allocated_amount > outstanding:
|
||||
@@ -761,10 +791,20 @@ class PaymentEntry(AccountsController):
|
||||
UPDATE `tabPayment Schedule`
|
||||
SET
|
||||
paid_amount = `paid_amount` + %s,
|
||||
base_paid_amount = `base_paid_amount` + %s,
|
||||
discounted_amount = `discounted_amount` + %s,
|
||||
outstanding = `outstanding` - %s
|
||||
outstanding = `outstanding` - %s,
|
||||
base_outstanding = `base_outstanding` - %s
|
||||
WHERE parent = %s and payment_term = %s""",
|
||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
||||
(
|
||||
allocated_amount - discounted_amt,
|
||||
base_paid_amount,
|
||||
discounted_amt,
|
||||
allocated_amount,
|
||||
base_outstanding,
|
||||
key[1],
|
||||
key[0],
|
||||
),
|
||||
)
|
||||
|
||||
def get_allocated_amount_in_transaction_currency(
|
||||
@@ -2879,7 +2919,7 @@ def get_payment_entry(
|
||||
pe.party_type = party_type
|
||||
pe.party = doc.get(scrub(party_type))
|
||||
pe.contact_person = doc.get("contact_person")
|
||||
pe.contact_email = doc.get("contact_email")
|
||||
complete_contact_details(pe)
|
||||
pe.ensure_supplier_is_not_blocked()
|
||||
|
||||
pe.paid_from = party_account if payment_type == "Receive" else bank.account
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
"paid_amount",
|
||||
"discounted_amount",
|
||||
"column_break_3",
|
||||
"base_payment_amount"
|
||||
"base_payment_amount",
|
||||
"base_outstanding",
|
||||
"base_paid_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -155,19 +157,35 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Payment Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_outstanding",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Outstanding (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "base_paid_amount",
|
||||
"fieldname": "base_paid_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Paid Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-16 13:57:06.382859",
|
||||
"modified": "2025-03-11 11:06:51.792982",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
|
||||
@@ -14,6 +14,8 @@ class PaymentSchedule(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
base_outstanding: DF.Currency
|
||||
base_paid_amount: DF.Currency
|
||||
base_payment_amount: DF.Currency
|
||||
description: DF.SmallText | None
|
||||
discount: DF.Float
|
||||
|
||||
@@ -8,6 +8,8 @@ from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
|
||||
|
||||
class RepostAccountingLedger(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -97,6 +99,9 @@ class RepostAccountingLedger(Document):
|
||||
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
|
||||
if doc.doctype in ["Payment Entry", "Journal Entry"]:
|
||||
gle_map = doc.build_gl_map()
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
warehouse_account_map = get_warehouse_account_map(doc.company)
|
||||
gle_map = doc.get_gl_entries(warehouse_account_map)
|
||||
else:
|
||||
gle_map = doc.get_gl_entries()
|
||||
|
||||
@@ -177,6 +182,14 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.force_set_against_expense_account()
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries(from_repost=True)
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
|
||||
@@ -12,6 +12,8 @@ from erpnext.accounts.doctype.payment_request.payment_request import make_paymen
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
|
||||
|
||||
|
||||
class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
@@ -204,9 +206,81 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
|
||||
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
|
||||
|
||||
def test_06_repost_purchase_receipt(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
provisional_account = create_account(
|
||||
account_name="Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
another_provisional_account = create_account(
|
||||
account_name="Another Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
company = frappe.get_doc("Company", self.company)
|
||||
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||
company.default_provisional_account = provisional_account
|
||||
company.save()
|
||||
|
||||
test_cc = company.cost_center
|
||||
default_expense_account = company.default_expense_account
|
||||
|
||||
item = make_item(properties={"is_stock_item": 0})
|
||||
|
||||
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0)
|
||||
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles = [
|
||||
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(expected_pr_gles, pr_gl_entries)
|
||||
|
||||
# change the provisional account
|
||||
frappe.db.set_value(
|
||||
"Purchase Receipt Item",
|
||||
pr.items[0].name,
|
||||
"provisional_expense_account",
|
||||
another_provisional_account,
|
||||
)
|
||||
|
||||
repost_doc = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_doc.company = self.company
|
||||
repost_doc.delete_cancelled_entries = True
|
||||
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
|
||||
repost_doc.save().submit()
|
||||
|
||||
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles_after_repost = [
|
||||
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": another_provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
|
||||
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
|
||||
|
||||
# teardown
|
||||
repost_doc.cancel()
|
||||
repost_doc.delete()
|
||||
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.default_provisional_account = None
|
||||
company.save()
|
||||
|
||||
|
||||
def update_repost_settings():
|
||||
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
allowed_types = [
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
"Purchase Receipt",
|
||||
]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
|
||||
@@ -1819,17 +1819,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
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",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
def test_invalid_currency(self):
|
||||
# Customer currency = USD
|
||||
|
||||
|
||||
@@ -279,9 +279,7 @@ def get_regional_address_details(party_details, doctype, company):
|
||||
pass
|
||||
|
||||
|
||||
def set_contact_details(party_details, party, party_type):
|
||||
party_details.contact_person = get_default_contact(party_type, party.name)
|
||||
|
||||
def complete_contact_details(party_details):
|
||||
if not party_details.contact_person:
|
||||
party_details.update(
|
||||
{
|
||||
@@ -310,6 +308,11 @@ def set_contact_details(party_details, party, party_type):
|
||||
party_details.update(contact_details)
|
||||
|
||||
|
||||
def set_contact_details(party_details, party, party_type):
|
||||
party_details.contact_person = get_default_contact(party_type, party.name)
|
||||
complete_contact_details(party_details)
|
||||
|
||||
|
||||
def set_other_values(party_details, party, party_type):
|
||||
# copy
|
||||
if party_type == "Customer":
|
||||
|
||||
@@ -517,7 +517,7 @@ class ReceivablePayableReport:
|
||||
select
|
||||
si.name, si.party_account_currency, si.currency, si.conversion_rate,
|
||||
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
|
||||
ps.description, ps.paid_amount, ps.discounted_amount
|
||||
ps.description, ps.paid_amount, ps.base_paid_amount, ps.discounted_amount
|
||||
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
|
||||
@@ -540,20 +540,24 @@ class ReceivablePayableReport:
|
||||
# Deduct that from paid amount pre allocation
|
||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||
|
||||
company_currency = frappe.get_value("Company", self.filters.get("company"), "default_currency")
|
||||
|
||||
# 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)
|
||||
self.append_payment_term(row, payment_terms_details[0], original_row, company_currency)
|
||||
return
|
||||
|
||||
for d in payment_terms_details:
|
||||
term = frappe._dict(original_row)
|
||||
self.append_payment_term(row, d, term)
|
||||
self.append_payment_term(row, d, term, company_currency)
|
||||
|
||||
def append_payment_term(self, row, d, term):
|
||||
if d.currency == d.party_account_currency:
|
||||
def append_payment_term(self, row, d, term, company_currency):
|
||||
invoiced = d.base_payment_amount
|
||||
paid_amount = d.base_paid_amount
|
||||
|
||||
if company_currency == d.party_account_currency or self.filters.get("in_party_currency"):
|
||||
invoiced = d.payment_amount
|
||||
else:
|
||||
invoiced = d.base_payment_amount
|
||||
paid_amount = d.paid_amount
|
||||
|
||||
row.payment_terms.append(
|
||||
term.update(
|
||||
@@ -562,15 +566,15 @@ class ReceivablePayableReport:
|
||||
"invoiced": invoiced,
|
||||
"invoice_grand_total": row.invoiced,
|
||||
"payment_term": d.description or d.payment_term,
|
||||
"paid": d.paid_amount + d.discounted_amount,
|
||||
"paid": paid_amount + d.discounted_amount,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": invoiced - d.paid_amount - d.discounted_amount,
|
||||
"outstanding": invoiced - paid_amount - d.discounted_amount,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if d.paid_amount:
|
||||
row["paid"] -= d.paid_amount + d.discounted_amount
|
||||
if paid_amount:
|
||||
row["paid"] -= paid_amount + d.discounted_amount
|
||||
|
||||
def allocate_closing_to_term(self, row, term, key):
|
||||
if row[key]:
|
||||
|
||||
@@ -145,6 +145,130 @@ def get_asset_categories_for_grouped_by_category(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
||||
finance_book_filter = ""
|
||||
if filters.get("finance_book"):
|
||||
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT results.asset_category,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabGL Entry` gle
|
||||
join `tabAsset` a on
|
||||
gle.against_voucher = a.name
|
||||
join `tabAsset Category Account` aca on
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition} {finance_book_filter}
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.asset_category) as results
|
||||
group by results.asset_category
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"finance_book": filters.get("finance_book", ""),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_group_by_asset_data(filters):
|
||||
data = []
|
||||
|
||||
asset_details = get_asset_details_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_asset(filters)
|
||||
|
||||
for asset_detail in asset_details:
|
||||
row = frappe._dict()
|
||||
row.update(asset_detail)
|
||||
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_asset_details_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
@@ -224,130 +348,6 @@ def get_asset_details_for_grouped_by_category(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_group_by_asset_data(filters):
|
||||
data = []
|
||||
|
||||
asset_details = get_asset_details_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_asset(filters)
|
||||
|
||||
for asset_detail in asset_details:
|
||||
row = frappe._dict()
|
||||
row.update(asset_detail)
|
||||
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
- flt(row.depreciation_eliminated_during_the_period)
|
||||
- flt(row.depreciation_eliminated_via_reversal)
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
|
||||
row.accumulated_depreciation_as_on_from_date
|
||||
)
|
||||
|
||||
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
|
||||
row.accumulated_depreciation_as_on_to_date
|
||||
)
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
||||
finance_book_filter = ""
|
||||
if filters.get("finance_book"):
|
||||
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT results.asset_category,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
|
||||
gle.credit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
|
||||
gle.debit
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabGL Entry` gle
|
||||
join `tabAsset` a on
|
||||
gle.against_voucher = a.name
|
||||
join `tabAsset Category Account` aca on
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition} {finance_book_filter}
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
0 as depreciation_eliminated_via_reversal,
|
||||
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
|
||||
a.opening_accumulated_depreciation
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.asset_category) as results
|
||||
group by results.asset_category
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"finance_book": filters.get("finance_book", ""),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def get_assets_for_grouped_by_asset(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
@@ -405,7 +405,7 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
group by a.name
|
||||
union
|
||||
SELECT a.name as name,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
|
||||
0
|
||||
else
|
||||
a.opening_accumulated_depreciation
|
||||
|
||||
@@ -9,8 +9,8 @@ import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import AliasedQuery, Criterion, Table
|
||||
from frappe.query_builder.functions import Count, Sum
|
||||
from frappe.query_builder import AliasedQuery, Case, Criterion, Table
|
||||
from frappe.query_builder.functions import Count, Max, Sum
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
@@ -1974,6 +1974,15 @@ class QueryPaymentLedger:
|
||||
.select(
|
||||
ple.against_voucher_no.as_("voucher_no"),
|
||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||
Max(
|
||||
Case().when(
|
||||
(
|
||||
(ple.voucher_no == ple.against_voucher_no)
|
||||
& (ple.voucher_type == ple.against_voucher_type)
|
||||
),
|
||||
(ple.posting_date),
|
||||
)
|
||||
).as_("invoice_date"),
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_against_voucher_no))
|
||||
@@ -1981,7 +1990,7 @@ class QueryPaymentLedger:
|
||||
.where(Criterion.all(self.dimensions_filter))
|
||||
.where(Criterion.all(self.voucher_posting_date))
|
||||
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
|
||||
.orderby(ple.posting_date, ple.voucher_no)
|
||||
.orderby(ple.invoice_date, ple.voucher_no)
|
||||
.having(qb.Field("amount_in_account_currency") > 0)
|
||||
.limit(self.limit)
|
||||
.run()
|
||||
|
||||
@@ -443,21 +443,22 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def validate_party_address_and_contact(self):
|
||||
party, party_type = None, None
|
||||
if self.get("customer"):
|
||||
party, party_type = self.customer, "Customer"
|
||||
party_type, party = self.get_party()
|
||||
|
||||
if not (party_type and party):
|
||||
return
|
||||
|
||||
if party_type == "Customer":
|
||||
billing_address, shipping_address = (
|
||||
self.get("customer_address"),
|
||||
self.get("shipping_address_name"),
|
||||
)
|
||||
self.validate_party_address(party, party_type, billing_address, shipping_address)
|
||||
elif self.get("supplier"):
|
||||
party, party_type = self.supplier, "Supplier"
|
||||
elif party_type == "Supplier":
|
||||
billing_address = self.get("supplier_address")
|
||||
self.validate_party_address(party, party_type, billing_address)
|
||||
|
||||
if party and party_type:
|
||||
self.validate_party_contact(party, party_type)
|
||||
self.validate_party_contact(party, party_type)
|
||||
|
||||
def validate_party_address(self, party, party_type, billing_address, shipping_address=None):
|
||||
if billing_address or shipping_address:
|
||||
@@ -2363,6 +2364,9 @@ class AccountsController(TransactionBase):
|
||||
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
||||
)
|
||||
d.outstanding = d.payment_amount
|
||||
d.base_outstanding = flt(
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_outstanding")
|
||||
)
|
||||
elif not d.invoice_portion:
|
||||
d.base_payment_amount = flt(
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||
@@ -2689,12 +2693,17 @@ class AccountsController(TransactionBase):
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if not default_currency:
|
||||
throw(_("Please enter default currency in Company Master"))
|
||||
if (
|
||||
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
|
||||
or not self.conversion_rate
|
||||
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
|
||||
):
|
||||
throw(_("Conversion rate cannot be 0 or 1"))
|
||||
|
||||
if not self.conversion_rate:
|
||||
throw(_("Conversion rate cannot be 0"))
|
||||
|
||||
if self.currency == default_currency and flt(self.conversion_rate) != 1.00:
|
||||
throw(_("Conversion rate must be 1.00 if document currency is same as company currency"))
|
||||
|
||||
if self.currency != default_currency and flt(self.conversion_rate) == 1.00:
|
||||
frappe.msgprint(
|
||||
_("Conversion rate is 1.00, but document currency is different from company currency")
|
||||
)
|
||||
|
||||
def check_finance_books(self, item, asset):
|
||||
if (
|
||||
|
||||
@@ -545,7 +545,11 @@ class SubcontractingController(StockController):
|
||||
def __get_batch_nos_for_bundle(self, qty, key):
|
||||
available_batches = defaultdict(float)
|
||||
|
||||
precision = frappe.get_precision("Subcontracting Receipt Supplied Item", "consumed_qty")
|
||||
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
|
||||
if flt(batch_qty, precision) <= 0:
|
||||
continue
|
||||
|
||||
qty_to_consumed = 0
|
||||
if qty > 0:
|
||||
if batch_qty >= qty:
|
||||
|
||||
@@ -69,7 +69,7 @@ def get_transaction_list(
|
||||
filters=None,
|
||||
limit_start=0,
|
||||
limit_page_length=20,
|
||||
order_by="modified",
|
||||
order_by="creation desc",
|
||||
custom=False,
|
||||
):
|
||||
user = frappe.session.user
|
||||
@@ -115,7 +115,7 @@ def get_transaction_list(
|
||||
limit_page_length,
|
||||
fields="name",
|
||||
ignore_permissions=ignore_permissions,
|
||||
order_by="modified desc",
|
||||
order_by=order_by,
|
||||
)
|
||||
|
||||
if custom:
|
||||
|
||||
@@ -2602,6 +2602,109 @@ class TestWorkOrder(FrappeTestCase):
|
||||
status = frappe.db.get_value("Serial No", row, "status")
|
||||
self.assertEqual(status, "Consumed")
|
||||
|
||||
def test_work_order_valuation_auto_pick(self):
|
||||
fg_item = "Test FG Item For Non Transfer Item Batch"
|
||||
rm_item = "Test RM Item For Non Transfer Item Batch"
|
||||
|
||||
make_item(fg_item, {"is_stock_item": 1})
|
||||
make_item(
|
||||
rm_item,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TST-BATCH-NTI-.###",
|
||||
},
|
||||
)
|
||||
|
||||
source_warehouse = "_Test Warehouse - _TC"
|
||||
wip_warehouse = "Stores - _TC"
|
||||
finished_goods_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company")
|
||||
|
||||
batches = make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse)
|
||||
|
||||
if not frappe.db.get_value("BOM", {"item": fg_item}):
|
||||
make_bom(item=fg_item, raw_materials=[rm_item])
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item=fg_item,
|
||||
qty=5,
|
||||
source_warehouse=source_warehouse,
|
||||
wip_warehouse=wip_warehouse,
|
||||
fg_warehouse=finished_goods_warehouse,
|
||||
)
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 5))
|
||||
stock_entry.items[0].batch_no = batches[1]
|
||||
stock_entry.items[0].use_serial_batch_fields = 1
|
||||
stock_entry.submit()
|
||||
stock_entry.reload()
|
||||
|
||||
self.assertEqual(stock_entry.items[0].valuation_rate, 200)
|
||||
|
||||
original_value = frappe.db.get_single_value(
|
||||
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
|
||||
)
|
||||
original_based_on = frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on")
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1)
|
||||
frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", "Expiry")
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 5))
|
||||
stock_entry.items[0].use_serial_batch_fields = 1
|
||||
stock_entry.submit()
|
||||
stock_entry.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
|
||||
self.assertEqual(batch_no, batches[1])
|
||||
self.assertEqual(stock_entry.items[0].valuation_rate, 200)
|
||||
self.assertEqual(stock_entry.items[1].valuation_rate, 200)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", original_value
|
||||
)
|
||||
frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", original_based_on)
|
||||
|
||||
|
||||
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
make_stock_entry as make_stock_entry_test_record,
|
||||
)
|
||||
|
||||
batches = []
|
||||
for qty, rate in ((5, 100), (5, 200)):
|
||||
stock_entry = make_stock_entry_test_record(
|
||||
item_code=rm_item,
|
||||
target=source_warehouse,
|
||||
qty=qty,
|
||||
basic_rate=rate,
|
||||
)
|
||||
stock_entry.submit()
|
||||
stock_entry.reload()
|
||||
|
||||
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
|
||||
batch_doc = frappe.get_doc("Batch", batch_no)
|
||||
|
||||
# keep early expiry date for the batch having rate 200
|
||||
days = 10 if rate == 100 else 1
|
||||
batch_doc.db_set("expiry_date", add_to_date(now(), days=days))
|
||||
|
||||
batches.append(batch_no)
|
||||
|
||||
stock_entry = make_stock_entry_test_record(
|
||||
item_code=rm_item,
|
||||
target=wip_warehouse,
|
||||
qty=qty,
|
||||
basic_rate=rate,
|
||||
)
|
||||
stock_entry.submit()
|
||||
stock_entry.reload()
|
||||
batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle)
|
||||
batch_doc = frappe.get_doc("Batch", batch_no)
|
||||
batch_doc.db_set("expiry_date", add_to_date(now(), days=10))
|
||||
|
||||
return batches
|
||||
|
||||
|
||||
def make_operation(**kwargs):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
@@ -397,7 +397,8 @@ erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
|
||||
erpnext.patches.v14_0.update_posting_datetime
|
||||
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
||||
erpnext.patches.v15_0.update_query_report
|
||||
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference #2025-03-18
|
||||
erpnext.patches.v15_0.recalculate_amount_difference_field
|
||||
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
|
||||
erpnext.patches.v15_0.recalculate_amount_difference_field #2025-03-18
|
||||
erpnext.patches.v15_0.rename_sla_fields #2025-03-12
|
||||
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item
|
||||
erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import frappe
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
|
||||
def execute():
|
||||
invoice_types = ["Sales Invoice", "Purchase Invoice"]
|
||||
for invoice_type in invoice_types:
|
||||
invoice = DocType(invoice_type)
|
||||
invoice_details = frappe.qb.from_(invoice).select(invoice.conversion_rate, invoice.name)
|
||||
update_payment_schedule(invoice_details)
|
||||
|
||||
|
||||
def update_payment_schedule(invoice_details):
|
||||
ps = DocType("Payment Schedule")
|
||||
|
||||
frappe.qb.update(ps).join(invoice_details).on(ps.parent == invoice_details.name).set(
|
||||
ps.base_paid_amount, ps.paid_amount * invoice_details.conversion_rate
|
||||
).set(ps.base_outstanding, ps.outstanding * invoice_details.conversion_rate).run()
|
||||
@@ -12,6 +12,7 @@
|
||||
"full_name",
|
||||
"welcome_email_sent",
|
||||
"view_attachments",
|
||||
"hide_timesheets",
|
||||
"section_break_5",
|
||||
"project_status"
|
||||
],
|
||||
@@ -64,6 +65,13 @@
|
||||
"in_list_view": 1,
|
||||
"label": "View attachments"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"default": "0",
|
||||
"fieldname": "hide_timesheets",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide timesheets"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
|
||||
@@ -335,7 +335,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
docstatus: ("<", 2),
|
||||
docstatus: ["<", 2],
|
||||
inspection_type: inspection_type,
|
||||
reference_name: doc.name,
|
||||
item_code: d.item_code
|
||||
|
||||
@@ -447,22 +447,21 @@ erpnext.sales_common = {
|
||||
args: { project: this.frm.doc.project },
|
||||
callback: function (r, rt) {
|
||||
if (!r.exc) {
|
||||
$.each(me.frm.doc["items"] || [], function (i, row) {
|
||||
if (r.message) {
|
||||
if (r.message) {
|
||||
$.each(me.frm.doc["items"] || [], function (i, row) {
|
||||
frappe.model.set_value(
|
||||
row.doctype,
|
||||
row.name,
|
||||
"cost_center",
|
||||
r.message
|
||||
);
|
||||
frappe.msgprint(
|
||||
__(
|
||||
"Cost Center For Item with Item Code {0} has been Changed to {1}",
|
||||
[row.item_name, r.message]
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
frappe.msgprint(
|
||||
__("Cost Center for Item rows has been updated to {0}", [
|
||||
r.message,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -195,6 +195,7 @@ class DeprecatedBatchNoValuation:
|
||||
|
||||
@deprecated
|
||||
def set_balance_value_for_non_batchwise_valuation_batches(self):
|
||||
self.last_sle = self.get_last_sle_for_non_batch()
|
||||
self.set_balance_value_from_sl_entries()
|
||||
self.set_balance_value_from_bundle()
|
||||
|
||||
@@ -242,17 +243,15 @@ class DeprecatedBatchNoValuation:
|
||||
for d in batch_data:
|
||||
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
||||
|
||||
last_sle = self.get_last_sle_for_non_batch()
|
||||
for d in batch_data:
|
||||
if self.available_qty.get(d.batch_no):
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value)
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction)
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(self.last_sle.stock_value)
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(self.last_sle.qty_after_transaction)
|
||||
|
||||
def get_last_sle_for_non_batch(self):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
if not self.sle.creation:
|
||||
@@ -267,8 +266,6 @@ class DeprecatedBatchNoValuation:
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.inner_join(batch)
|
||||
.on(sle.batch_no == batch.name)
|
||||
.select(
|
||||
sle.stock_value,
|
||||
sle.qty_after_transaction,
|
||||
@@ -276,7 +273,6 @@ class DeprecatedBatchNoValuation:
|
||||
.where(
|
||||
(sle.item_code == self.sle.item_code)
|
||||
& (sle.warehouse == self.sle.warehouse)
|
||||
& (sle.batch_no.isnotnull())
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
.where(timestamp_condition)
|
||||
@@ -289,60 +285,8 @@ class DeprecatedBatchNoValuation:
|
||||
query = query.where(sle.name != self.sle.name)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
return data[0] if data else {}
|
||||
|
||||
@deprecated
|
||||
def get_last_sle_for_sabb_no_batchwise_valuation(self):
|
||||
sabb = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
sabb_entry = frappe.qb.DocType("Serial and Batch Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
posting_datetime = CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||
timestamp_condition = CombineDatetime(sabb.posting_date, sabb.posting_time) < posting_datetime
|
||||
|
||||
if self.sle.creation:
|
||||
timestamp_condition |= (
|
||||
CombineDatetime(sabb.posting_date, sabb.posting_time) == posting_datetime
|
||||
) & (sabb.creation < self.sle.creation)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sabb)
|
||||
.inner_join(sabb_entry)
|
||||
.on(sabb.name == sabb_entry.parent)
|
||||
.inner_join(batch)
|
||||
.on(sabb_entry.batch_no == batch.name)
|
||||
.select(sabb.name)
|
||||
.where(
|
||||
(sabb.item_code == self.sle.item_code)
|
||||
& (sabb.warehouse == self.sle.warehouse)
|
||||
& (sabb_entry.batch_no.isnotnull())
|
||||
& (sabb.is_cancelled == 0)
|
||||
& (sabb.docstatus == 1)
|
||||
)
|
||||
.where(timestamp_condition)
|
||||
.orderby(sabb.posting_date, order=Order.desc)
|
||||
.orderby(sabb.posting_time, order=Order.desc)
|
||||
.orderby(sabb.creation, order=Order.desc)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
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 {}
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"serial_and_batch_bundle": data[0].name},
|
||||
["stock_value", "qty_after_transaction"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return sle if sle else {}
|
||||
return data[0] if data else frappe._dict()
|
||||
|
||||
@deprecated
|
||||
def set_balance_value_from_bundle(self) -> None:
|
||||
@@ -393,10 +337,9 @@ class DeprecatedBatchNoValuation:
|
||||
for d in batch_data:
|
||||
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
||||
|
||||
last_sle = self.get_last_sle_for_sabb_no_batchwise_valuation()
|
||||
if not last_sle:
|
||||
if not self.last_sle:
|
||||
return
|
||||
|
||||
for batch_no in self.available_qty:
|
||||
self.non_batchwise_balance_value[batch_no] = flt(last_sle.stock_value)
|
||||
self.non_batchwise_balance_qty[batch_no] = flt(last_sle.qty_after_transaction)
|
||||
self.non_batchwise_balance_value[batch_no] = flt(self.last_sle.stock_value)
|
||||
self.non_batchwise_balance_qty[batch_no] = flt(self.last_sle.qty_after_transaction)
|
||||
|
||||
@@ -197,8 +197,19 @@ class QualityInspection(Document):
|
||||
self.quality_inspection_template = template
|
||||
self.get_item_specification_details()
|
||||
|
||||
def on_update(self):
|
||||
if (
|
||||
frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
|
||||
== "Warn"
|
||||
):
|
||||
self.update_qc_reference()
|
||||
|
||||
def on_submit(self):
|
||||
self.update_qc_reference()
|
||||
if (
|
||||
frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
|
||||
== "Stop"
|
||||
):
|
||||
self.update_qc_reference()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = "Serial and Batch Bundle"
|
||||
@@ -206,15 +217,15 @@ class QualityInspection(Document):
|
||||
self.update_qc_reference()
|
||||
|
||||
def on_trash(self):
|
||||
self.update_qc_reference()
|
||||
self.update_qc_reference(remove_reference=True)
|
||||
|
||||
def validate_readings_status_mandatory(self):
|
||||
for reading in self.readings:
|
||||
if not reading.status:
|
||||
frappe.throw(_("Row #{0}: Status is mandatory").format(reading.idx))
|
||||
|
||||
def update_qc_reference(self):
|
||||
quality_inspection = self.name if self.docstatus == 1 else ""
|
||||
def update_qc_reference(self, remove_reference=False):
|
||||
quality_inspection = self.name if self.docstatus < 2 and not remove_reference else ""
|
||||
|
||||
if self.reference_type == "Job Card":
|
||||
if self.reference_name:
|
||||
@@ -244,7 +255,7 @@ class QualityInspection(Document):
|
||||
)
|
||||
)
|
||||
|
||||
if self.batch_no and self.docstatus == 1:
|
||||
if self.batch_no and self.docstatus < 2:
|
||||
query = query.where(child_doc.batch_no == self.batch_no)
|
||||
|
||||
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
|
||||
|
||||
@@ -2103,7 +2103,8 @@ def get_auto_batch_nos(kwargs):
|
||||
filter_zero_near_batches(available_batches, kwargs)
|
||||
|
||||
if not kwargs.consider_negative_batches:
|
||||
available_batches = list(filter(lambda x: x.qty > 0, available_batches))
|
||||
precision = frappe.get_precision("Stock Ledger Entry", "actual_qty")
|
||||
available_batches = [d for d in available_batches if flt(d.qty, precision) > 0]
|
||||
|
||||
if not qty:
|
||||
return available_batches
|
||||
|
||||
@@ -1026,10 +1026,6 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
};
|
||||
});
|
||||
|
||||
if (me.frm.doc.company && erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) {
|
||||
this.frm.add_fetch("company", "stock_adjustment_account", "expense_account");
|
||||
}
|
||||
|
||||
this.frm.fields_dict.items.grid.get_field("expense_account").get_query = function () {
|
||||
if (erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) {
|
||||
return {
|
||||
@@ -1143,8 +1139,6 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
this.frm.trigger("toggle_display_account_head");
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
if (this.frm.doc.company && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company))
|
||||
this.set_default_account("stock_adjustment_account", "expense_account");
|
||||
this.set_default_account("cost_center", "cost_center");
|
||||
|
||||
this.frm.refresh_fields("items");
|
||||
|
||||
@@ -1672,7 +1672,7 @@ class StockEntry(StockController):
|
||||
if self.purpose == "Material Issue":
|
||||
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
|
||||
|
||||
if self.purpose == "Manufacture":
|
||||
if self.purpose == "Manufacture" or not ret.get("expense_account"):
|
||||
ret["expense_account"] = frappe.get_cached_value(
|
||||
"Company", self.company, "stock_adjustment_account"
|
||||
)
|
||||
|
||||
@@ -250,6 +250,7 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fetch_from": "item_code.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
@@ -588,7 +589,8 @@
|
||||
"label": "Serial and Batch Bundle",
|
||||
"no_copy": 1,
|
||||
"options": "Serial and Batch Bundle",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -606,11 +608,12 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-25 15:58:40.982582",
|
||||
"modified": "2025-03-26 21:01:58.544797",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
@@ -620,4 +623,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1212,9 +1212,21 @@ class update_entries_after:
|
||||
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
|
||||
|
||||
# Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
|
||||
if not sle.dependant_sle_voucher_detail_no:
|
||||
if not sle.dependant_sle_voucher_detail_no or self.is_manufacture_entry_with_sabb(sle):
|
||||
self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no)
|
||||
|
||||
def is_manufacture_entry_with_sabb(self, sle):
|
||||
if (
|
||||
self.args.get("sle_id")
|
||||
and sle.serial_and_batch_bundle
|
||||
and sle.auto_created_serial_and_batch_bundle
|
||||
):
|
||||
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
|
||||
if purpose in ["Manufacture", "Repack"]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def recalculate_amounts_in_stock_entry(self, voucher_no, voucher_detail_no):
|
||||
stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
|
||||
stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
{{ empty_state(_("Task")) }}
|
||||
{% endif %}
|
||||
|
||||
<h4 class="my-account-header">{{ _("Timesheets") }}</h4>
|
||||
{% if doc.timesheets %}
|
||||
<h4 class="my-account-header">{{ _("Timesheets") }}</h4>
|
||||
<div class="website-list">
|
||||
<div class="result">
|
||||
<div class="web-list-item transaction-list-item">
|
||||
@@ -73,8 +73,6 @@
|
||||
{% include "erpnext/templates/includes/projects/project_timesheets.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ empty_state(_("Timesheet")) }}
|
||||
{% endif %}
|
||||
|
||||
{% if doc.attachments %}
|
||||
@@ -136,4 +134,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -9,7 +9,7 @@ def get_context(context):
|
||||
project_user = frappe.db.get_value(
|
||||
"Project User",
|
||||
{"parent": frappe.form_dict.project, "user": frappe.session.user},
|
||||
["user", "view_attachments"],
|
||||
["user", "view_attachments", "hide_timesheets"],
|
||||
as_dict=True,
|
||||
)
|
||||
if frappe.session.user != "Administrator" and (not project_user or frappe.session.user == "Guest"):
|
||||
@@ -25,7 +25,8 @@ def get_context(context):
|
||||
project.name, start=0, item_status="open", search=frappe.form_dict.get("search")
|
||||
)
|
||||
|
||||
project.timesheets = get_timesheets(project.name, start=0, search=frappe.form_dict.get("search"))
|
||||
if project_user and not project_user.hide_timesheets:
|
||||
project.timesheets = get_timesheets(project.name, start=0, search=frappe.form_dict.get("search"))
|
||||
|
||||
if project_user and project_user.view_attachments:
|
||||
project.attachments = get_attachments(project.name)
|
||||
|
||||
Reference in New Issue
Block a user