mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-14 12:38:46 +00:00
Merge pull request #45357 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
syscohada_countries = [
|
||||
"bj", # Bénin
|
||||
"bf", # Burkina-Faso
|
||||
"cm", # Cameroun
|
||||
"cf", # Centrafrique
|
||||
"ci", # Côte d'Ivoire
|
||||
"cg", # Congo
|
||||
"km", # Comores
|
||||
"ga", # Gabon
|
||||
"gn", # Guinée
|
||||
"gw", # Guinée-Bissau
|
||||
"gq", # Guinée Equatoriale
|
||||
"ml", # Mali
|
||||
"ne", # Niger
|
||||
"cd", # République Démocratique du Congo
|
||||
"sn", # Sénégal
|
||||
"td", # Tchad
|
||||
"tg", # Togo
|
||||
]
|
||||
|
||||
folder = Path(__file__).parent
|
||||
generic_charts = Path(folder).glob("syscohada*.json")
|
||||
|
||||
for file in generic_charts:
|
||||
with open(file) as f:
|
||||
chart = json.load(f)
|
||||
for country in syscohada_countries:
|
||||
chart["country_code"] = country
|
||||
json_object = json.dumps(chart, indent=4)
|
||||
with open(Path(folder, file.name.replace("syscohada", country)), "w") as outfile:
|
||||
outfile.write(json_object)
|
||||
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
@@ -12,7 +12,7 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
msg += " ";
|
||||
msg += __("Please enable only if the understand the effects of enabling this.");
|
||||
msg += "<br>";
|
||||
msg += "Do you still want to enable immutable ledger?";
|
||||
msg += __("Do you still want to enable immutable ledger?");
|
||||
|
||||
frappe.confirm(
|
||||
msg,
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length",
|
||||
"payment_request_settings",
|
||||
@@ -515,6 +516,13 @@
|
||||
"fieldname": "reconciliation_queue_size",
|
||||
"fieldtype": "Int",
|
||||
"label": "Reconciliation Queue Size"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Ignores legacy Is Opening field in GL Entry that allows adding opening balance post the system is in use while generating reports",
|
||||
"fieldname": "ignore_is_opening_check_for_reporting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Is Opening check for reporting"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -522,7 +530,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-13 17:38:39.661320",
|
||||
"modified": "2025-01-18 21:24:19.840745",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -48,6 +48,7 @@ class AccountsSettings(Document):
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
ignore_is_opening_check_for_reporting: DF.Check
|
||||
make_payment_via_journal_entry: DF.Check
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
|
||||
@@ -114,10 +114,10 @@ class Subscription(Document):
|
||||
|
||||
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
|
||||
elif date:
|
||||
_current_invoice_start = date
|
||||
elif self.trial_period_start and self.is_trialling():
|
||||
_current_invoice_start = self.trial_period_start
|
||||
else:
|
||||
_current_invoice_start = nowdate()
|
||||
|
||||
@@ -414,8 +414,8 @@ class Subscription(Document):
|
||||
if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
|
||||
invoice.apply_tds = 1
|
||||
|
||||
# Add party currency to invoice
|
||||
invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
|
||||
# Add currency to invoice
|
||||
invoice.currency = frappe.db.get_value("Subscription Plan", {"name": self.plans[0].plan}, "currency")
|
||||
|
||||
# Add dimensions in invoice for subscription:
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils.data import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -470,6 +470,28 @@ class TestSubscription(FrappeTestCase):
|
||||
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
|
||||
self.assertEqual(currency, "USD")
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1},
|
||||
)
|
||||
def test_multi_currency_subscription_with_default_company_currency(self):
|
||||
party = "Test Subscription Customer Multi Currency"
|
||||
frappe.db.set_value("Customer", party, "default_currency", "USD")
|
||||
subscription = create_subscription(
|
||||
start_date="2018-01-01",
|
||||
generate_invoice_at="Beginning of the current subscription period",
|
||||
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}],
|
||||
party=party,
|
||||
)
|
||||
|
||||
subscription.process(posting_date="2018-01-01")
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, "Unpaid")
|
||||
|
||||
# Check the currency of the created invoice
|
||||
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
|
||||
self.assertEqual(currency, "USD")
|
||||
|
||||
def test_subscription_recovery(self):
|
||||
"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
|
||||
subscription = create_subscription(
|
||||
@@ -581,6 +603,12 @@ def create_parties():
|
||||
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"})
|
||||
customer.insert()
|
||||
|
||||
if not frappe.db.exists("Customer", "_Test Subscription Customer Multi Currency"):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "Test Subscription Customer Multi Currency"
|
||||
customer.default_currency = "USD"
|
||||
customer.insert()
|
||||
|
||||
if not frappe.db.exists("Customer", "_Test Subscription Customer John Doe"):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Subscription Customer John Doe"
|
||||
|
||||
@@ -156,6 +156,9 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
}
|
||||
)
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
inv.round_off_applicable_accounts_for_tax_withholding = tax_details.account_head
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
return tax_row, tax_deducted_on_advances, voucher_wise_amount
|
||||
else:
|
||||
@@ -302,6 +305,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
# if tds account is changed.
|
||||
if not tax_deducted:
|
||||
tax_deducted = is_tax_deducted_on_the_basis_of_inv(vouchers)
|
||||
|
||||
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
net_total = inv.tax_withholding_net_total
|
||||
@@ -336,6 +343,18 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
|
||||
|
||||
|
||||
def is_tax_deducted_on_the_basis_of_inv(vouchers):
|
||||
return frappe.db.exists(
|
||||
"Purchase Taxes and Charges",
|
||||
{
|
||||
"parent": ["in", vouchers],
|
||||
"is_tax_withholding_account": 1,
|
||||
"parenttype": "Purchase Invoice",
|
||||
"base_tax_amount_after_discount_amount": [">", 0],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
|
||||
field = (
|
||||
|
||||
@@ -61,6 +61,49 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_with_account_changed(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Multi Account TDS Category"
|
||||
)
|
||||
invoices = []
|
||||
|
||||
# create invoices for lower than single threshold tax rate
|
||||
for _ in range(2):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier")
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# create another invoice whose total when added to previously created invoice,
|
||||
# surpasses cumulative threshhold
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier")
|
||||
pi.submit()
|
||||
|
||||
# assert equal tax deduction on total invoice amount until now
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 3000)
|
||||
self.assertEqual(pi.grand_total, 7000)
|
||||
invoices.append(pi)
|
||||
|
||||
# account changed
|
||||
|
||||
frappe.db.set_value(
|
||||
"Tax Withholding Account",
|
||||
{"parent": "Multi Account TDS Category"},
|
||||
"account",
|
||||
"_Test Account VAT - _TC",
|
||||
)
|
||||
|
||||
# TDS should be on invoice only even though account is changed
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
|
||||
pi.submit()
|
||||
|
||||
# assert equal tax deduction on total invoice amount until now
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 500)
|
||||
invoices.append(pi)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_single_threshold_tds(self):
|
||||
invoices = []
|
||||
frappe.db.set_value(
|
||||
@@ -1061,6 +1104,16 @@ def create_tax_withholding_category_records():
|
||||
consider_party_ledger_amount=1,
|
||||
)
|
||||
|
||||
create_tax_withholding_category(
|
||||
category_name="Multi Account TDS Category",
|
||||
rate=10,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
account="TDS - _TC",
|
||||
single_threshold=0,
|
||||
cumulative_threshold=30000,
|
||||
)
|
||||
|
||||
|
||||
def create_tax_withholding_category(
|
||||
category_name,
|
||||
|
||||
@@ -676,7 +676,7 @@ def set_taxes(
|
||||
):
|
||||
from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template
|
||||
|
||||
args = {party_type.lower(): party, "company": company}
|
||||
args = {frappe.scrub(party_type): party, "company": company}
|
||||
|
||||
if tax_category:
|
||||
args["tax_category"] = tax_category
|
||||
@@ -696,10 +696,10 @@ def set_taxes(
|
||||
else:
|
||||
args.update(get_party_details(party, party_type))
|
||||
|
||||
if party_type in ("Customer", "Lead", "Prospect"):
|
||||
if party_type in ("Customer", "Lead", "Prospect", "CRM Deal"):
|
||||
args.update({"tax_type": "Sales"})
|
||||
|
||||
if party_type in ["Lead", "Prospect"]:
|
||||
if party_type in ["Lead", "Prospect", "CRM Deal"]:
|
||||
args["customer"] = None
|
||||
del args[frappe.scrub(party_type)]
|
||||
else:
|
||||
|
||||
@@ -510,12 +510,16 @@ def get_accounting_entries(
|
||||
.where(gl_entry.company == filters.company)
|
||||
)
|
||||
|
||||
ignore_is_opening = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_is_opening_check_for_reporting"
|
||||
)
|
||||
|
||||
if doctype == "GL Entry":
|
||||
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
|
||||
query = query.where(gl_entry.is_cancelled == 0)
|
||||
query = query.where(gl_entry.posting_date <= to_date)
|
||||
|
||||
if ignore_opening_entries:
|
||||
if ignore_opening_entries and not ignore_is_opening:
|
||||
query = query.where(gl_entry.is_opening == "No")
|
||||
else:
|
||||
query = query.select(gl_entry.closing_date.as_("posting_date"))
|
||||
|
||||
@@ -208,6 +208,10 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
|
||||
ignore_is_opening = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_is_opening_check_for_reporting"
|
||||
)
|
||||
|
||||
if filters.get("account"):
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
if filters.account:
|
||||
@@ -270,9 +274,15 @@ def get_conditions(filters):
|
||||
or filters.get("party")
|
||||
or filters.get("group_by") in ["Group by Account", "Group by Party"]
|
||||
):
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
else:
|
||||
conditions.append("posting_date >=%(from_date)s")
|
||||
|
||||
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
|
||||
else:
|
||||
conditions.append("posting_date <=%(to_date)s")
|
||||
|
||||
if filters.get("project"):
|
||||
conditions.append("project in %(project)s")
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Trial Balance",
|
||||
"report_type": "Script Report",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
|
||||
@@ -89,6 +89,10 @@ def get_data(filters):
|
||||
)
|
||||
company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company)
|
||||
|
||||
ignore_is_opening = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_is_opening_check_for_reporting"
|
||||
)
|
||||
|
||||
if not accounts:
|
||||
return None
|
||||
|
||||
@@ -96,7 +100,7 @@ def get_data(filters):
|
||||
|
||||
gl_entries_by_account = {}
|
||||
|
||||
opening_balances = get_opening_balances(filters)
|
||||
opening_balances = get_opening_balances(filters, ignore_is_opening)
|
||||
|
||||
# add filter inside list so that the query in financial_statements.py doesn't break
|
||||
if filters.project:
|
||||
@@ -114,7 +118,13 @@ def get_data(filters):
|
||||
ignore_opening_entries=True,
|
||||
)
|
||||
|
||||
calculate_values(accounts, gl_entries_by_account, opening_balances, filters.get("show_net_values"))
|
||||
calculate_values(
|
||||
accounts,
|
||||
gl_entries_by_account,
|
||||
opening_balances,
|
||||
filters.get("show_net_values"),
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name)
|
||||
|
||||
data = prepare_data(accounts, filters, parent_children_map, company_currency)
|
||||
@@ -125,15 +135,15 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_opening_balances(filters):
|
||||
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet")
|
||||
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss")
|
||||
def get_opening_balances(filters, ignore_is_opening):
|
||||
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet", ignore_is_opening)
|
||||
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss", ignore_is_opening)
|
||||
|
||||
balance_sheet_opening.update(pl_opening)
|
||||
return balance_sheet_opening
|
||||
|
||||
|
||||
def get_rootwise_opening_balances(filters, report_type):
|
||||
def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
|
||||
gle = []
|
||||
|
||||
last_period_closing_voucher = ""
|
||||
@@ -159,16 +169,24 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
period_closing_voucher=last_period_closing_voucher[0].name,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
)
|
||||
|
||||
# Report getting generate from the mid of a fiscal year
|
||||
if getdate(last_period_closing_voucher[0].period_end_date) < getdate(add_days(filters.from_date, -1)):
|
||||
start_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
|
||||
gle += get_opening_balance(
|
||||
"GL Entry", filters, report_type, accounting_dimensions, start_date=start_date
|
||||
"GL Entry",
|
||||
filters,
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
start_date=start_date,
|
||||
ignore_is_opening=ignore_is_opening,
|
||||
)
|
||||
else:
|
||||
gle = get_opening_balance("GL Entry", filters, report_type, accounting_dimensions)
|
||||
gle = get_opening_balance(
|
||||
"GL Entry", filters, report_type, accounting_dimensions, ignore_is_opening=ignore_is_opening
|
||||
)
|
||||
|
||||
opening = frappe._dict()
|
||||
for d in gle:
|
||||
@@ -187,7 +205,13 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
|
||||
|
||||
def get_opening_balance(
|
||||
doctype, filters, report_type, accounting_dimensions, period_closing_voucher=None, start_date=None
|
||||
doctype,
|
||||
filters,
|
||||
report_type,
|
||||
accounting_dimensions,
|
||||
period_closing_voucher=None,
|
||||
start_date=None,
|
||||
ignore_is_opening=0,
|
||||
):
|
||||
closing_balance = frappe.qb.DocType(doctype)
|
||||
account = frappe.qb.DocType("Account")
|
||||
@@ -223,11 +247,16 @@ def get_opening_balance(
|
||||
(closing_balance.posting_date >= start_date)
|
||||
& (closing_balance.posting_date < filters.from_date)
|
||||
)
|
||||
opening_balance = opening_balance.where(closing_balance.is_opening == "No")
|
||||
|
||||
if not ignore_is_opening:
|
||||
opening_balance = opening_balance.where(closing_balance.is_opening == "No")
|
||||
else:
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
|
||||
)
|
||||
if not ignore_is_opening:
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
|
||||
)
|
||||
else:
|
||||
opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date)
|
||||
|
||||
if doctype == "GL Entry":
|
||||
opening_balance = opening_balance.where(closing_balance.is_cancelled == 0)
|
||||
@@ -298,7 +327,7 @@ def get_opening_balance(
|
||||
return gle
|
||||
|
||||
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values):
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values, ignore_is_opening=0):
|
||||
init = {
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
@@ -316,7 +345,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
|
||||
d["opening_credit"] = opening_balances.get(d.name, {}).get("opening_credit", 0)
|
||||
|
||||
for entry in gl_entries_by_account.get(d.name, []):
|
||||
if cstr(entry.is_opening) != "Yes":
|
||||
if cstr(entry.is_opening) != "Yes" or ignore_is_opening:
|
||||
d["debit"] += flt(entry.debit)
|
||||
d["credit"] += flt(entry.credit)
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ def validate_returned_items(doc):
|
||||
for d in doc.get("items"):
|
||||
key = d.item_code
|
||||
raise_exception = False
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice"]:
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice", "POS Invoice"]:
|
||||
field = frappe.scrub(doc.doctype) + "_item"
|
||||
if d.get(field):
|
||||
key = (d.item_code, d.get(field))
|
||||
@@ -259,7 +259,7 @@ def get_already_returned_items(doc):
|
||||
)
|
||||
data = frappe.db.sql(
|
||||
f"""
|
||||
select {column}, {field}
|
||||
select {column}, child.{field}
|
||||
from
|
||||
`tab{doc.doctype} Item` child, `tab{doc.doctype}` par
|
||||
where
|
||||
@@ -1020,7 +1020,7 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
|
||||
|
||||
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
qty_field = "stock_qty"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
@@ -1109,7 +1109,7 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
qty_field = "stock_qty"
|
||||
|
||||
warehouse = child_doc.get(warehouse_field)
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
|
||||
@@ -30,6 +30,11 @@ class calculate_taxes_and_totals:
|
||||
"Accounts Settings", "round_row_wise_tax"
|
||||
)
|
||||
|
||||
if doc.get("round_off_applicable_accounts_for_tax_withholding"):
|
||||
frappe.flags.round_off_applicable_accounts.append(
|
||||
doc.round_off_applicable_accounts_for_tax_withholding
|
||||
)
|
||||
|
||||
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
||||
|
||||
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
"quotation_section",
|
||||
"default_valid_till",
|
||||
"section_break_13",
|
||||
"carry_forward_communication_and_comments"
|
||||
"carry_forward_communication_and_comments",
|
||||
"column_break_junk",
|
||||
"update_timestamp_on_new_communication"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -77,7 +79,7 @@
|
||||
{
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Other Settings"
|
||||
"label": "Activity"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -85,13 +87,24 @@
|
||||
"fieldname": "carry_forward_communication_and_comments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Carry Forward Communication and Comments"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_junk",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Update the modified timestamp on new communications received in Lead & Opportunity.",
|
||||
"fieldname": "update_timestamp_on_new_communication",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update timestamp on new communication"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-06 11:22:08.464253",
|
||||
"modified": "2025-01-16 16:12:14.889455",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Settings",
|
||||
|
||||
@@ -20,6 +20,7 @@ class CRMSettings(Document):
|
||||
carry_forward_communication_and_comments: DF.Check
|
||||
close_opportunity_after_days: DF.Int
|
||||
default_valid_till: DF.Data | None
|
||||
update_timestamp_on_new_communication: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -84,6 +84,20 @@ def link_communications_with_prospect(communication, method):
|
||||
row.db_update()
|
||||
|
||||
|
||||
def update_modified_timestamp(communication, method):
|
||||
if communication.reference_doctype and communication.reference_name:
|
||||
if communication.sent_or_received == "Received" and frappe.db.get_single_value(
|
||||
"CRM Settings", "update_timestamp_on_new_communication"
|
||||
):
|
||||
frappe.db.set_value(
|
||||
dt=communication.reference_doctype,
|
||||
dn=communication.reference_name,
|
||||
field="modified",
|
||||
val=now(),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
|
||||
def get_linked_prospect(reference_doctype, reference_name):
|
||||
prospect = None
|
||||
if reference_doctype == "Lead":
|
||||
|
||||
@@ -29,7 +29,7 @@ frappe.ui.form.on("Plaid Settings", {
|
||||
"Bank Transaction",
|
||||
"",
|
||||
true,
|
||||
"Bank Transaction"
|
||||
__("Bank Transaction")
|
||||
);
|
||||
|
||||
frappe.msgprint({
|
||||
|
||||
@@ -351,7 +351,10 @@ doc_events = {
|
||||
"erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update",
|
||||
"erpnext.support.doctype.issue.issue.set_first_response_time",
|
||||
],
|
||||
"after_insert": "erpnext.crm.utils.link_communications_with_prospect",
|
||||
"after_insert": [
|
||||
"erpnext.crm.utils.link_communications_with_prospect",
|
||||
"erpnext.crm.utils.update_modified_timestamp",
|
||||
],
|
||||
},
|
||||
"Event": {
|
||||
"after_insert": "erpnext.crm.utils.link_events_with_prospect",
|
||||
|
||||
@@ -43,6 +43,7 @@ class BlanketOrder(Document):
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_duplicate_items()
|
||||
self.validate_item_qty()
|
||||
self.set_party_item_code()
|
||||
|
||||
def validate_dates(self):
|
||||
@@ -117,6 +118,11 @@ class BlanketOrder(Document):
|
||||
for d in self.items:
|
||||
d.db_set("ordered_qty", item_ordered_qty.get(d.item_code, 0))
|
||||
|
||||
def validate_item_qty(self):
|
||||
for d in self.items:
|
||||
if d.qty < 0:
|
||||
frappe.throw(_("Row {0}: Quantity cannot be negative.").format(d.idx))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_order(source_name):
|
||||
@@ -148,7 +154,7 @@ def make_order(source_name):
|
||||
"doctype": doctype + " Item",
|
||||
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0,
|
||||
"condition": lambda item: not (flt(item.qty)) or (flt(item.qty) - flt(item.ordered_qty)) > 0,
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -186,7 +192,7 @@ def validate_against_blanket_order(order_doc):
|
||||
if item.item_code in item_data:
|
||||
remaining_qty = item.qty - item.ordered_qty
|
||||
allowed_qty = remaining_qty + (remaining_qty * (allowance / 100))
|
||||
if allowed_qty < item_data[item.item_code]:
|
||||
if item.qty and allowed_qty < item_data[item.item_code]:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Item {0} cannot be ordered more than {1} against Blanket Order {2}."
|
||||
|
||||
@@ -1549,6 +1549,9 @@ def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None):
|
||||
fields=["bom_no", "qty"],
|
||||
order_by="idx asc",
|
||||
)
|
||||
# fetch Scrap Items for Parent Bom
|
||||
items = get_bom_items_as_dict(bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1)
|
||||
scrap_items.update(items)
|
||||
|
||||
for row in bom_items:
|
||||
if not row.bom_no:
|
||||
|
||||
@@ -755,6 +755,19 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items)
|
||||
self.assertTrue("_Test RM Item 3 Manufacture Item" in items)
|
||||
|
||||
def test_get_scrap_items_from_sub_assemblies(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_scrap_items_from_sub_assemblies
|
||||
|
||||
bom = frappe.copy_doc(test_records[1])
|
||||
bom.insert(ignore_mandatory=True)
|
||||
|
||||
bom_scraped_items = [i.get("item_code") for i in bom.get("scrap_items", [])]
|
||||
|
||||
# get scrapted items for parent bom
|
||||
scraped_items = get_scrap_items_from_sub_assemblies(bom.name, bom.company, 2, None)
|
||||
for item_code in scraped_items.keys():
|
||||
self.assertIn(item_code, bom_scraped_items, f"Item {item_code} not found in BOM scrap items")
|
||||
|
||||
def test_bom_raw_materials_stock_uom(self):
|
||||
rm_item = make_item(
|
||||
properties={"is_stock_item": 1, "valuation_rate": 1000.0, "stock_uom": "Nos"}
|
||||
|
||||
@@ -204,7 +204,7 @@ class BOMCreator(Document):
|
||||
|
||||
for field, label in fields.items():
|
||||
if not self.get(field):
|
||||
frappe.throw(_("Please set {0} in BOM Creator {1}").format(label, self.name))
|
||||
frappe.throw(_("Please set {0} in BOM Creator {1}").format(_(label), self.name))
|
||||
|
||||
def on_submit(self):
|
||||
self.enqueue_create_boms()
|
||||
@@ -365,12 +365,6 @@ def get_children(doctype=None, parent=None, **kwargs):
|
||||
return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
|
||||
|
||||
|
||||
def get_parent_row_no(doc, name):
|
||||
for row in doc.items:
|
||||
if row.name == name:
|
||||
return row.idx
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_item(**kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
@@ -418,6 +412,8 @@ def add_sub_assembly(**kwargs):
|
||||
parent_row_no = ""
|
||||
if not kwargs.convert_to_sub_assembly:
|
||||
item_info = get_item_details(bom_item.item_code)
|
||||
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||
|
||||
item_row = doc.append(
|
||||
"items",
|
||||
{
|
||||
@@ -426,6 +422,7 @@ def add_sub_assembly(**kwargs):
|
||||
"uom": item_info.stock_uom,
|
||||
"fg_item": kwargs.fg_item,
|
||||
"conversion_factor": 1,
|
||||
"parent_row_no": parent_row_no,
|
||||
"fg_reference_id": name,
|
||||
"stock_qty": bom_item.qty,
|
||||
"do_not_explode": 1,
|
||||
@@ -437,9 +434,7 @@ def add_sub_assembly(**kwargs):
|
||||
parent_row_no = item_row.idx
|
||||
name = ""
|
||||
else:
|
||||
parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
|
||||
if parent_row_no:
|
||||
parent_row_no = parent_row_no[0]
|
||||
parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
|
||||
|
||||
for row in bom_item.get("items"):
|
||||
row = frappe._dict(row)
|
||||
@@ -471,6 +466,14 @@ def get_item_details(item_code):
|
||||
)
|
||||
|
||||
|
||||
def get_parent_row_no(doc, name):
|
||||
for row in doc.items:
|
||||
if row.name == name:
|
||||
return row.idx
|
||||
|
||||
frappe.msgprint(_("Parent Row No not found for {0}").format(name))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_node(**kwargs):
|
||||
if isinstance(kwargs, str):
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"capacity_planning_for_days",
|
||||
"mins_between_operations",
|
||||
"other_settings_section",
|
||||
"set_op_cost_and_scrape_from_sub_assemblies",
|
||||
"set_op_cost_and_scrap_from_sub_assemblies",
|
||||
"column_break_23",
|
||||
"make_serial_no_batch_from_work_order"
|
||||
],
|
||||
@@ -202,13 +202,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Excess Material Transfer"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.",
|
||||
"fieldname": "set_op_cost_and_scrape_from_sub_assemblies",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Operating Cost / Scrap Items From Sub-assemblies"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.material_consumption",
|
||||
@@ -243,13 +236,20 @@
|
||||
"fieldname": "validate_components_quantities_per_bom",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Components and Quantities Per BOM"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "To include sub-assembly costs and scrap items in Finished Goods on a work order without using a job card, when the 'Use Multi-Level BOM' option is enabled.",
|
||||
"fieldname": "set_op_cost_and_scrap_from_sub_assemblies",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Operating Cost / Scrap Items From Sub-assemblies"
|
||||
}
|
||||
],
|
||||
"icon": "icon-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-09 16:02:23.326763",
|
||||
"modified": "2025-01-13 12:07:03.089977",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
|
||||
@@ -34,7 +34,7 @@ class ManufacturingSettings(Document):
|
||||
mins_between_operations: DF.Int
|
||||
overproduction_percentage_for_sales_order: DF.Percent
|
||||
overproduction_percentage_for_work_order: DF.Percent
|
||||
set_op_cost_and_scrape_from_sub_assemblies: DF.Check
|
||||
set_op_cost_and_scrap_from_sub_assemblies: DF.Check
|
||||
update_bom_costs_automatically: DF.Check
|
||||
validate_components_quantities_per_bom: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -2091,7 +2091,7 @@ class TestWorkOrder(FrappeTestCase):
|
||||
def test_op_cost_and_scrap_based_on_sub_assemblies(self):
|
||||
# Make Sub Assembly BOM 1
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 1)
|
||||
frappe.db.set_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies", 1)
|
||||
|
||||
items = {
|
||||
"Test Final FG Item": 0,
|
||||
@@ -2132,7 +2132,7 @@ class TestWorkOrder(FrappeTestCase):
|
||||
for row in se_doc.additional_costs:
|
||||
self.assertEqual(row.amount, 3000)
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0)
|
||||
frappe.db.set_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies", 0)
|
||||
|
||||
@change_settings(
|
||||
"Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1}
|
||||
@@ -2501,6 +2501,53 @@ class TestWorkOrder(FrappeTestCase):
|
||||
manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
|
||||
self.assertEqual(manufacture_entry.items[0].s_warehouse, "Stores - _TC")
|
||||
|
||||
def test_serial_no_status_for_stock_entry(self):
|
||||
items = {
|
||||
"Finished Good Test Item 1": {"is_stock_item": 1},
|
||||
"_Test RM Item with Serial No": {
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-FCG-NO-.####",
|
||||
},
|
||||
}
|
||||
for item, properties in items.items():
|
||||
make_item(item, properties)
|
||||
|
||||
fg_item = "Finished Good Test Item 1"
|
||||
rec_se = test_stock_entry.make_stock_entry(
|
||||
item_code="_Test RM Item with Serial No", target="_Test Warehouse - _TC", qty=4, basic_rate=100
|
||||
)
|
||||
|
||||
if not frappe.db.get_value("BOM", {"item": fg_item, "docstatus": 1}):
|
||||
bom = make_bom(
|
||||
item=fg_item,
|
||||
rate=1000,
|
||||
raw_materials=["_Test RM Item with Serial No"],
|
||||
do_not_save=True,
|
||||
)
|
||||
bom.rm_cost_as_per = "Price List" # non stock item won't have valuation rate
|
||||
bom.buying_price_list = "_Test Price List India"
|
||||
bom.currency = "INR"
|
||||
bom.save()
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
production_item=fg_item, skip_transfer=1, source_warehouse="_Test Warehouse - _TC"
|
||||
)
|
||||
|
||||
ste = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 4))
|
||||
ste.items[0].use_serial_batch_fields = 1
|
||||
ste.items[0].serial_no = "\n".join(
|
||||
get_serial_nos_from_bundle(rec_se.items[0].serial_and_batch_bundle)
|
||||
)
|
||||
ste.insert()
|
||||
ste.submit()
|
||||
|
||||
ste.reload()
|
||||
serial_nos = get_serial_nos_from_bundle(ste.items[0].serial_and_batch_bundle)
|
||||
for row in serial_nos:
|
||||
status = frappe.db.get_value("Serial No", row, "status")
|
||||
self.assertEqual(status, "Consumed")
|
||||
|
||||
|
||||
def make_operation(**kwargs):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
@@ -348,7 +348,7 @@ class WorkOrder(Document):
|
||||
if flt(self.material_transferred_for_manufacturing) > 0:
|
||||
status = "In Process"
|
||||
|
||||
total_qty = flt(self.produced_qty) + flt(self.process_loss_qty)
|
||||
total_qty = flt(flt(self.produced_qty) + flt(self.process_loss_qty), self.precision("qty"))
|
||||
if flt(total_qty) >= flt(self.qty):
|
||||
status = "Completed"
|
||||
else:
|
||||
|
||||
@@ -315,7 +315,7 @@ erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
|
||||
erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
|
||||
erpnext.patches.v14_0.set_period_start_end_date_in_pcv
|
||||
erpnext.patches.v14_0.update_closing_balances #08-11-2024
|
||||
erpnext.patches.v14_0.update_closing_balances #20-12-2024
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
||||
erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts
|
||||
erpnext.patches.v14_0.update_subscription_details
|
||||
@@ -387,5 +387,6 @@ erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions
|
||||
erpnext.patches.v15_0.enable_allow_existing_serial_no
|
||||
erpnext.patches.v15_0.update_cc_in_process_statement_of_accounts
|
||||
erpnext.patches.v15_0.update_asset_status_to_work_in_progress
|
||||
erpnext.patches.v15_0.rename_manufacturing_settings_field
|
||||
erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect
|
||||
erpnext.patches.v15_0.sync_auto_reconcile_config
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import itertools
|
||||
|
||||
import frappe
|
||||
|
||||
@@ -10,34 +11,91 @@ from erpnext.accounts.doctype.account_closing_balance.account_closing_balance im
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
def execute():
|
||||
# clear balances, they will be recalculated
|
||||
frappe.db.truncate("Account Closing Balance")
|
||||
|
||||
gle_fields = get_gle_fields()
|
||||
pcv_list = get_period_closing_vouchers()
|
||||
gl_entries = get_gl_entries(pcv_list)
|
||||
|
||||
for company in frappe.get_all("Company", pluck="name"):
|
||||
i = 0
|
||||
company_wise_order = {}
|
||||
for pcv in get_period_closing_vouchers(company):
|
||||
company_wise_order.setdefault(pcv.company, [])
|
||||
if pcv.period_end_date not in company_wise_order[pcv.company]:
|
||||
pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name)
|
||||
pcv_doc.pl_accounts_reverse_gle = get_pcv_gl_entries_for_pl_accounts(pcv, gle_fields)
|
||||
pcv_doc.closing_account_gle = get_pcv_gl_entries_for_closing_accounts(pcv, gle_fields)
|
||||
closing_entries = pcv_doc.get_account_closing_balances()
|
||||
make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date)
|
||||
for _, pcvs in itertools.groupby(pcv_list, key=lambda pcv: (pcv.company, pcv.period_start_date)):
|
||||
process_grouped_pcvs(list(pcvs), gl_entries)
|
||||
|
||||
company_wise_order[pcv.company].append(pcv.period_end_date)
|
||||
i += 1
|
||||
|
||||
def process_grouped_pcvs(pcvs, gl_entries):
|
||||
pl_account_entries = []
|
||||
closing_account_entries = []
|
||||
first_pcv = pcvs[0]
|
||||
|
||||
for pcv in pcvs:
|
||||
pcv_entries = gl_entries.get(pcv.name) or []
|
||||
for entry in pcv_entries:
|
||||
entry["closing_date"] = first_pcv.period_end_date
|
||||
entry["period_closing_voucher"] = first_pcv.name
|
||||
entry["voucher_no"] = first_pcv.name
|
||||
|
||||
list_to_update = (
|
||||
pl_account_entries if entry.account != pcv.closing_account_head else closing_account_entries
|
||||
)
|
||||
list_to_update.append(entry)
|
||||
|
||||
# hacky!!
|
||||
if to_cancel := pcvs[1:]:
|
||||
to_cancel = [pcv.name for pcv in to_cancel]
|
||||
|
||||
# update voucher number
|
||||
gle_to_update = [entry.name for entry in closing_account_entries + pl_account_entries]
|
||||
frappe.db.set_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"name": ("in", gle_to_update),
|
||||
"voucher_no": ("in", to_cancel),
|
||||
},
|
||||
"voucher_no",
|
||||
first_pcv.name,
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
# mark as cancelled
|
||||
frappe.db.set_value(
|
||||
"Period Closing Voucher",
|
||||
{"name": ("in", to_cancel)},
|
||||
"docstatus",
|
||||
2,
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
pcv_doc = frappe.get_doc("Period Closing Voucher", first_pcv.name)
|
||||
pcv_doc.pl_accounts_reverse_gle = pl_account_entries
|
||||
pcv_doc.closing_account_gle = closing_account_entries
|
||||
closing_entries = pcv_doc.get_account_closing_balances()
|
||||
make_closing_entries(closing_entries, pcv_doc.name, pcv_doc.company, pcv_doc.period_end_date)
|
||||
|
||||
|
||||
def get_period_closing_vouchers():
|
||||
return frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
fields=["name", "closing_account_head", "period_start_date", "period_end_date", "company"],
|
||||
filters={"docstatus": 1},
|
||||
order_by="period_start_date asc, period_end_date desc",
|
||||
)
|
||||
|
||||
|
||||
def get_gl_entries(pcv_list):
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": ("in", [pcv.name for pcv in pcv_list]), "is_cancelled": 0},
|
||||
fields=get_gle_fields(),
|
||||
update={"is_period_closing_voucher_entry": 1},
|
||||
)
|
||||
|
||||
return {k: list(v) for k, v in itertools.groupby(gl_entries, key=lambda gle: gle.voucher_no)}
|
||||
|
||||
|
||||
def get_gle_fields():
|
||||
default_diemnsion_fields = ["cost_center", "finance_book", "project"]
|
||||
accounting_dimension_fields = get_accounting_dimensions()
|
||||
gle_fields = [
|
||||
return [
|
||||
"name",
|
||||
"company",
|
||||
"posting_date",
|
||||
@@ -47,43 +105,11 @@ def get_gle_fields():
|
||||
"credit",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
*default_diemnsion_fields,
|
||||
*accounting_dimension_fields,
|
||||
"voucher_no",
|
||||
# default dimension fields
|
||||
"cost_center",
|
||||
"finance_book",
|
||||
"project",
|
||||
# accounting dimensions
|
||||
*get_accounting_dimensions(),
|
||||
]
|
||||
|
||||
return gle_fields
|
||||
|
||||
|
||||
def get_period_closing_vouchers(company):
|
||||
return frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
fields=["name", "closing_account_head", "period_start_date", "period_end_date", "company"],
|
||||
filters={"docstatus": 1, "company": company},
|
||||
order_by="period_end_date",
|
||||
)
|
||||
|
||||
|
||||
def get_pcv_gl_entries_for_pl_accounts(pcv, gle_fields):
|
||||
return get_gl_entries(pcv, gle_fields, {"account": ["!=", pcv.closing_account_head]})
|
||||
|
||||
|
||||
def get_pcv_gl_entries_for_closing_accounts(pcv, gle_fields):
|
||||
return get_gl_entries(pcv, gle_fields, {"account": pcv.closing_account_head})
|
||||
|
||||
|
||||
def get_gl_entries(pcv, gle_fields, accounts_filter=None):
|
||||
filters = {"voucher_no": pcv.name, "is_cancelled": 0}
|
||||
if accounts_filter:
|
||||
filters.update(accounts_filter)
|
||||
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters=filters,
|
||||
fields=gle_fields,
|
||||
)
|
||||
for entry in gl_entries:
|
||||
entry["is_period_closing_voucher_entry"] = 1
|
||||
entry["closing_date"] = pcv.period_end_date
|
||||
entry["period_closing_voucher"] = pcv.name
|
||||
|
||||
return gl_entries
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
rename_field(
|
||||
"Manufacturing Settings",
|
||||
"set_op_cost_and_scrape_from_sub_assemblies",
|
||||
"set_op_cost_and_scrap_from_sub_assemblies",
|
||||
)
|
||||
@@ -9,7 +9,7 @@ from frappe.desk.reportview import get_match_cond
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Interval
|
||||
from frappe.query_builder.functions import Count, CurDate, Date, Sum, UnixTimestamp
|
||||
from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
|
||||
from frappe.utils import add_days, flt, get_datetime, get_link_to_form, get_time, get_url, nowtime, today
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
from erpnext import get_default_company
|
||||
@@ -341,24 +341,19 @@ class Project(Document):
|
||||
frappe.db.set_value("Project", new_name, "copied_from", new_name)
|
||||
|
||||
def send_welcome_email(self):
|
||||
url = get_url(f"/project/?name={self.name}")
|
||||
messages = (
|
||||
_("You have been invited to collaborate on the project: {0}").format(self.name),
|
||||
url,
|
||||
_("Join"),
|
||||
)
|
||||
label = f"{self.project_name} ({self.name})"
|
||||
url = get_link_to_form(self.doctype, self.name, label)
|
||||
|
||||
content = """
|
||||
<p>{0}.</p>
|
||||
<p><a href="{1}">{2}</a></p>
|
||||
"""
|
||||
content = "<p>{}</p>".format(
|
||||
_("You have been invited to collaborate on the project: {0}").format(url)
|
||||
)
|
||||
|
||||
for user in self.users:
|
||||
if user.welcome_email_sent == 0:
|
||||
frappe.sendmail(
|
||||
user.user,
|
||||
subject=_("Project Collaboration Invitation"),
|
||||
content=content.format(*messages),
|
||||
content=content,
|
||||
)
|
||||
user.welcome_email_sent = 1
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe import _
|
||||
from frappe.query_builder import Interval
|
||||
from frappe.query_builder.functions import Count, CurDate, UnixTimestamp
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.data import get_url_to_list
|
||||
from frappe.utils.nestedset import NestedSet, get_root_of
|
||||
|
||||
from erpnext import get_default_currency
|
||||
@@ -42,6 +43,9 @@ class SalesPerson(NestedSet):
|
||||
nsm_parent_field = "parent_sales_person"
|
||||
|
||||
def validate(self):
|
||||
if not self.enabled:
|
||||
self.validate_sales_person()
|
||||
|
||||
if not self.parent_sales_person:
|
||||
self.parent_sales_person = get_root_of("Sales Person")
|
||||
|
||||
@@ -83,6 +87,25 @@ class SalesPerson(NestedSet):
|
||||
super().on_update()
|
||||
self.validate_one_root()
|
||||
|
||||
def validate_sales_person(self):
|
||||
sales_team = frappe.qb.DocType("Sales Team")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sales_team)
|
||||
.select(sales_team.sales_person)
|
||||
.where((sales_team.sales_person == self.name) & (sales_team.parenttype == "Customer"))
|
||||
.groupby(sales_team.sales_person)
|
||||
).run(as_dict=True)
|
||||
|
||||
if query:
|
||||
frappe.throw(
|
||||
_("The Sales Person is linked with {0}").format(
|
||||
frappe.bold(
|
||||
f"""<a href="{get_url_to_list("Customer")}?sales_person={self.name}">{"Customers"}</a>"""
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def get_email_id(self):
|
||||
if self.employee:
|
||||
user = frappe.db.get_value("Employee", self.employee, "user_id")
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, flt, get_datetime_str, nowdate
|
||||
from frappe.utils.data import now_datetime
|
||||
from frappe.utils.data import getdate, now_datetime
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext import get_default_company
|
||||
|
||||
PEGGED_CURRENCIES = {
|
||||
"USD": {"AED": 3.6725}, # AED is pegged to USD at a rate of 3.6725 since 1997
|
||||
}
|
||||
|
||||
|
||||
def before_tests():
|
||||
frappe.clear_cache()
|
||||
@@ -45,6 +49,14 @@ def before_tests():
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_pegged_rate(from_currency: str, to_currency: str, transaction_date) -> float | None:
|
||||
if rate := PEGGED_CURRENCIES.get(from_currency, {}).get(to_currency):
|
||||
return rate
|
||||
elif rate := PEGGED_CURRENCIES.get(to_currency, {}).get(from_currency):
|
||||
return 1 / rate
|
||||
return None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=None):
|
||||
if not (from_currency and to_currency):
|
||||
@@ -55,6 +67,10 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
|
||||
if not transaction_date:
|
||||
transaction_date = nowdate()
|
||||
|
||||
if rate := get_pegged_rate(from_currency, to_currency, transaction_date):
|
||||
return rate
|
||||
|
||||
currency_settings = frappe.get_doc("Accounts Settings").as_dict()
|
||||
allow_stale_rates = currency_settings.get("allow_stale")
|
||||
|
||||
@@ -95,8 +111,8 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
settings = frappe.get_cached_doc("Currency Exchange Settings")
|
||||
req_params = {
|
||||
"transaction_date": transaction_date,
|
||||
"from_currency": from_currency,
|
||||
"to_currency": to_currency,
|
||||
"from_currency": from_currency if from_currency != "AED" else "USD",
|
||||
"to_currency": to_currency if to_currency != "AED" else "USD",
|
||||
}
|
||||
params = {}
|
||||
for row in settings.req_params:
|
||||
@@ -108,6 +124,14 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
for res_key in settings.result_key:
|
||||
value = value[format_ces_api(str(res_key.key), req_params)]
|
||||
cache.setex(name=key, time=21600, value=flt(value))
|
||||
|
||||
# Support AED conversion through pegged USD
|
||||
value = flt(value)
|
||||
if to_currency == "AED":
|
||||
value *= 3.6725
|
||||
if from_currency == "AED":
|
||||
value /= 3.6725
|
||||
|
||||
return flt(value)
|
||||
except Exception:
|
||||
frappe.log_error("Unable to fetch exchange rate")
|
||||
|
||||
@@ -74,9 +74,8 @@ class PickList(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_for_qty()
|
||||
if self.pick_manually and self.get("locations"):
|
||||
self.validate_stock_qty()
|
||||
self.check_serial_no_status()
|
||||
self.validate_stock_qty()
|
||||
self.check_serial_no_status()
|
||||
|
||||
def before_save(self):
|
||||
self.update_status()
|
||||
@@ -90,14 +89,24 @@ class PickList(Document):
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
for row in self.get("locations"):
|
||||
if row.batch_no and not row.qty:
|
||||
if not row.picked_qty:
|
||||
continue
|
||||
|
||||
if row.batch_no and row.picked_qty:
|
||||
batch_qty = get_batch_qty(row.batch_no, row.warehouse, row.item_code)
|
||||
|
||||
if row.qty > batch_qty:
|
||||
if row.picked_qty > batch_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}."
|
||||
).format(row.idx, row.item_code, batch_qty, row.batch_no, bold(row.warehouse)),
|
||||
"At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}. Please restock the item."
|
||||
).format(
|
||||
row.idx,
|
||||
row.picked_qty,
|
||||
row.item_code,
|
||||
batch_qty,
|
||||
row.batch_no,
|
||||
bold(row.warehouse),
|
||||
),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
@@ -109,11 +118,11 @@ class PickList(Document):
|
||||
"actual_qty",
|
||||
)
|
||||
|
||||
if row.qty > flt(bin_qty):
|
||||
if row.picked_qty > flt(bin_qty):
|
||||
frappe.throw(
|
||||
_(
|
||||
"At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}."
|
||||
).format(row.idx, row.qty, bold(row.item_code), bin_qty, bold(row.warehouse)),
|
||||
).format(row.idx, row.picked_qty, bold(row.item_code), bin_qty, bold(row.warehouse)),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
@@ -429,7 +438,14 @@ class PickList(Document):
|
||||
locations_replica = self.get("locations")
|
||||
|
||||
# reset
|
||||
self.delete_key("locations")
|
||||
reset_rows = []
|
||||
for row in self.get("locations"):
|
||||
if not row.picked_qty:
|
||||
reset_rows.append(row)
|
||||
|
||||
for row in reset_rows:
|
||||
self.remove(row)
|
||||
|
||||
updated_locations = frappe._dict()
|
||||
for item_doc in items:
|
||||
item_code = item_doc.item_code
|
||||
@@ -499,6 +515,9 @@ class PickList(Document):
|
||||
# aggregate qty for same item
|
||||
item_map = OrderedDict()
|
||||
for item in locations:
|
||||
if item.picked_qty:
|
||||
continue
|
||||
|
||||
if not item.item_code:
|
||||
frappe.throw(f"Row #{item.idx}: Item Code is Mandatory")
|
||||
if not cint(
|
||||
|
||||
@@ -870,7 +870,7 @@ 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"))
|
||||
self.assertFalse(pl.locations)
|
||||
|
||||
def test_pick_list_validation_for_serial_no(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
@@ -901,7 +901,7 @@ 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"))
|
||||
self.assertFalse(pl.locations)
|
||||
|
||||
def test_pick_list_validation_for_batch_no(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
@@ -937,7 +937,7 @@ 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"))
|
||||
self.assertFalse(pl.locations)
|
||||
|
||||
def test_pick_list_validation_for_batch_no_and_serial_item(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
@@ -977,7 +977,7 @@ 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"))
|
||||
self.assertFalse(pl.locations)
|
||||
|
||||
def test_pick_list_validation_for_multiple_batches_and_sales_order(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
@@ -1172,6 +1172,7 @@ class TestPickList(FrappeTestCase):
|
||||
|
||||
for row in pl.locations:
|
||||
row.qty = row.qty + 10
|
||||
row.picked_qty = row.qty
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pl.save)
|
||||
|
||||
@@ -1266,3 +1267,42 @@ class TestPickList(FrappeTestCase):
|
||||
delivery_note = create_delivery_note(pl.name)
|
||||
|
||||
self.assertEqual(len(delivery_note.items), 1)
|
||||
|
||||
def test_pick_list_not_reset_batch(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(
|
||||
"Test Do Not Reset Picked Item",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BTH-PICKLT-.######",
|
||||
},
|
||||
).name
|
||||
|
||||
se = make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||
batch1 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
se = make_stock_entry(item=item, to_warehouse=warehouse, qty=10)
|
||||
batch2 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
|
||||
for loc in pl.locations:
|
||||
self.assertEqual(loc.batch_no, batch1)
|
||||
loc.batch_no = batch2
|
||||
loc.picked_qty = 0.0
|
||||
|
||||
pl.save()
|
||||
|
||||
for loc in pl.locations:
|
||||
self.assertEqual(loc.batch_no, batch1)
|
||||
loc.batch_no = batch2
|
||||
loc.picked_qty = 10.0
|
||||
|
||||
pl.save()
|
||||
|
||||
for loc in pl.locations:
|
||||
self.assertEqual(loc.batch_no, batch2)
|
||||
|
||||
@@ -4,12 +4,26 @@
|
||||
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
|
||||
|
||||
frappe.ui.form.on("Quality Inspection", {
|
||||
onload(frm) {
|
||||
frm.trigger("set_default_company");
|
||||
},
|
||||
|
||||
set_default_company(frm) {
|
||||
if (!frm.doc.company) {
|
||||
frm.set_value("company", frappe.defaults.get_default("company"));
|
||||
}
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
frm.set_query("reference_name", function () {
|
||||
frm.set_query("reference_name", function (doc) {
|
||||
let filters = { docstatus: ["!=", 2] };
|
||||
|
||||
if (doc.company) {
|
||||
filters["company"] = doc.company;
|
||||
}
|
||||
|
||||
return {
|
||||
filters: {
|
||||
docstatus: ["!=", 2],
|
||||
},
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"company",
|
||||
"report_date",
|
||||
"status",
|
||||
"manual_inspection",
|
||||
"child_row_reference",
|
||||
"column_break_4",
|
||||
"inspection_type",
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"child_row_reference",
|
||||
"section_break_7",
|
||||
"item_code",
|
||||
"item_serial_no",
|
||||
@@ -27,6 +27,7 @@
|
||||
"bom_no",
|
||||
"specification_details",
|
||||
"quality_inspection_template",
|
||||
"manual_inspection",
|
||||
"readings",
|
||||
"section_break_14",
|
||||
"inspected_by",
|
||||
@@ -248,6 +249,12 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-search",
|
||||
@@ -255,7 +262,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-30 19:08:16.611192",
|
||||
"modified": "2025-01-16 17:00:48.774532",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Quality Inspection",
|
||||
|
||||
@@ -30,6 +30,7 @@ class QualityInspection(Document):
|
||||
batch_no: DF.Link | None
|
||||
bom_no: DF.Link | None
|
||||
child_row_reference: DF.Data | None
|
||||
company: DF.Link | None
|
||||
description: DF.SmallText | None
|
||||
inspected_by: DF.Link
|
||||
inspection_type: DF.Literal["", "Incoming", "Outgoing", "In Process"]
|
||||
@@ -76,6 +77,13 @@ class QualityInspection(Document):
|
||||
|
||||
self.validate_inspection_required()
|
||||
self.set_child_row_reference()
|
||||
self.set_company()
|
||||
|
||||
def set_company(self):
|
||||
if self.reference_type and self.reference_name:
|
||||
company = frappe.get_cached_value(self.reference_type, self.reference_name, "company")
|
||||
if company != self.company:
|
||||
self.company = company
|
||||
|
||||
def set_child_row_reference(self):
|
||||
if self.child_row_reference:
|
||||
|
||||
@@ -350,7 +350,7 @@ class SerialandBatchBundle(Document):
|
||||
for row in bundle_data:
|
||||
if row.serial_no:
|
||||
valuation_details["serial_nos"][row.serial_no] = row.incoming_rate
|
||||
else:
|
||||
if row.batch_no:
|
||||
valuation_details["batches"][row.batch_no] = row.incoming_rate
|
||||
|
||||
return valuation_details
|
||||
|
||||
@@ -254,7 +254,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "\nActive\nInactive\nDelivered\nExpired",
|
||||
"options": "\nActive\nInactive\nConsumed\nDelivered\nExpired",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -272,7 +272,7 @@
|
||||
"icon": "fa fa-barcode",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-17 10:52:55.767839",
|
||||
"modified": "2025-01-15 16:22:49.873889",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No",
|
||||
@@ -316,4 +316,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class SerialNo(StockController):
|
||||
purchase_document_no: DF.Data | None
|
||||
purchase_rate: DF.Float
|
||||
serial_no: DF.Data
|
||||
status: DF.Literal["", "Active", "Inactive", "Delivered", "Expired"]
|
||||
status: DF.Literal["", "Active", "Inactive", "Consumed", "Delivered", "Expired"]
|
||||
warehouse: DF.Link | None
|
||||
warranty_expiry_date: DF.Date | None
|
||||
warranty_period: DF.Int
|
||||
|
||||
@@ -2040,7 +2040,7 @@ class StockEntry(StockController):
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||
|
||||
if (
|
||||
frappe.db.get_single_value("Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies")
|
||||
frappe.db.get_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies")
|
||||
and self.work_order
|
||||
and frappe.get_cached_value("Work Order", self.work_order, "use_multi_level_bom")
|
||||
):
|
||||
@@ -2847,7 +2847,7 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
||||
if (
|
||||
bom_no
|
||||
and frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies"
|
||||
"Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies"
|
||||
)
|
||||
and frappe.get_cached_value("Work Order", work_order, "use_multi_level_bom")
|
||||
):
|
||||
|
||||
@@ -1802,7 +1802,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
|
||||
for serial_no in serial_nos:
|
||||
self.assertTrue(frappe.db.exists("Serial No", serial_no))
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Consumed")
|
||||
|
||||
def test_serial_batch_bundle_type_of_transaction(self):
|
||||
item = make_item(
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-08-24 16:00:22.696958",
|
||||
"modified": "2025-01-15 16:00:22.696958",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Type",
|
||||
|
||||
@@ -89,18 +89,22 @@ def get_average_age(fifo_queue: list, to_date: str) -> float:
|
||||
|
||||
def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> list:
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
|
||||
range_values = [0.0] * (len(filters.ranges) + 1)
|
||||
range_values = [0.0] * ((len(filters.ranges) * 2) + 2)
|
||||
|
||||
for item in fifo_queue:
|
||||
age = flt(date_diff(to_date, item[1]))
|
||||
qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0
|
||||
stock_value = flt(item[2])
|
||||
|
||||
for i, age_limit in enumerate(filters.ranges):
|
||||
if age <= flt(age_limit):
|
||||
i *= 2
|
||||
range_values[i] = flt(range_values[i] + qty, precision)
|
||||
range_values[i + 1] = flt(range_values[i + 1] + stock_value, precision)
|
||||
break
|
||||
else:
|
||||
range_values[-1] = flt(range_values[-1] + qty, precision)
|
||||
range_values[-2] = flt(range_values[-2] + qty, precision)
|
||||
range_values[-1] = flt(range_values[-1] + stock_value, precision)
|
||||
|
||||
return range_values
|
||||
|
||||
@@ -199,6 +203,7 @@ def setup_ageing_columns(filters: Filters, range_columns: list):
|
||||
for i, label in enumerate(ranges):
|
||||
fieldname = "range" + str(i + 1)
|
||||
add_column(range_columns, label=_("Age ({0})").format(label), fieldname=fieldname)
|
||||
add_column(range_columns, label=_("Value ({0})").format(label), fieldname=fieldname + "value")
|
||||
|
||||
|
||||
def add_column(range_columns: list, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140):
|
||||
@@ -298,16 +303,22 @@ class FIFOSlots:
|
||||
# neutralize 0/negative stock by adding positive stock
|
||||
fifo_queue[0][0] += flt(row.actual_qty)
|
||||
fifo_queue[0][1] = row.posting_date
|
||||
fifo_queue[0][2] += flt(row.stock_value_difference)
|
||||
else:
|
||||
fifo_queue.append([flt(row.actual_qty), row.posting_date])
|
||||
fifo_queue.append(
|
||||
[flt(row.actual_qty), row.posting_date, flt(row.stock_value_difference)]
|
||||
)
|
||||
return
|
||||
|
||||
valuation = row.stock_value_difference / row.actual_qty
|
||||
for serial_no in serial_nos:
|
||||
if self.serial_no_batch_purchase_details.get(serial_no):
|
||||
fifo_queue.append([serial_no, self.serial_no_batch_purchase_details.get(serial_no)])
|
||||
fifo_queue.append(
|
||||
[serial_no, self.serial_no_batch_purchase_details.get(serial_no), valuation]
|
||||
)
|
||||
else:
|
||||
self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date)
|
||||
fifo_queue.append([serial_no, row.posting_date])
|
||||
fifo_queue.append([serial_no, row.posting_date, valuation])
|
||||
|
||||
def __compute_outgoing_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list):
|
||||
"Update FIFO Queue on outward stock."
|
||||
@@ -316,34 +327,44 @@ class FIFOSlots:
|
||||
return
|
||||
|
||||
qty_to_pop = abs(row.actual_qty)
|
||||
stock_value = abs(row.stock_value_difference)
|
||||
|
||||
while qty_to_pop:
|
||||
slot = fifo_queue[0] if fifo_queue else [0, None]
|
||||
slot = fifo_queue[0] if fifo_queue else [0, None, 0]
|
||||
if 0 < flt(slot[0]) <= qty_to_pop:
|
||||
# qty to pop >= slot qty
|
||||
# if +ve and not enough or exactly same balance in current slot, consume whole slot
|
||||
qty_to_pop -= flt(slot[0])
|
||||
stock_value -= flt(slot[2])
|
||||
self.transferred_item_details[transfer_key].append(fifo_queue.pop(0))
|
||||
elif not fifo_queue:
|
||||
# negative stock, no balance but qty yet to consume
|
||||
fifo_queue.append([-(qty_to_pop), row.posting_date])
|
||||
self.transferred_item_details[transfer_key].append([qty_to_pop, row.posting_date])
|
||||
fifo_queue.append([-(qty_to_pop), row.posting_date, -(stock_value)])
|
||||
self.transferred_item_details[transfer_key].append(
|
||||
[qty_to_pop, row.posting_date, stock_value]
|
||||
)
|
||||
qty_to_pop = 0
|
||||
stock_value = 0
|
||||
else:
|
||||
# qty to pop < slot qty, ample balance
|
||||
# consume actual_qty from first slot
|
||||
slot[0] = flt(slot[0]) - qty_to_pop
|
||||
self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]])
|
||||
slot[2] = flt(slot[2]) - stock_value
|
||||
self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1], stock_value])
|
||||
qty_to_pop = 0
|
||||
stock_value = 0
|
||||
|
||||
def __adjust_incoming_transfer_qty(self, transfer_data: dict, fifo_queue: list, row: dict):
|
||||
"Add previously removed stock back to FIFO Queue."
|
||||
transfer_qty_to_pop = flt(row.actual_qty)
|
||||
stock_value = flt(row.stock_value_difference)
|
||||
|
||||
def add_to_fifo_queue(slot):
|
||||
if fifo_queue and flt(fifo_queue[0][0]) <= 0:
|
||||
# neutralize 0/negative stock by adding positive stock
|
||||
fifo_queue[0][0] += flt(slot[0])
|
||||
fifo_queue[0][1] = slot[1]
|
||||
fifo_queue[0][2] += flt(slot[2])
|
||||
else:
|
||||
fifo_queue.append(slot)
|
||||
|
||||
@@ -351,16 +372,20 @@ class FIFOSlots:
|
||||
if transfer_data and 0 < transfer_data[0][0] <= transfer_qty_to_pop:
|
||||
# bucket qty is not enough, consume whole
|
||||
transfer_qty_to_pop -= transfer_data[0][0]
|
||||
stock_value -= transfer_data[0][2]
|
||||
add_to_fifo_queue(transfer_data.pop(0))
|
||||
elif not transfer_data:
|
||||
# transfer bucket is empty, extra incoming qty
|
||||
add_to_fifo_queue([transfer_qty_to_pop, row.posting_date])
|
||||
add_to_fifo_queue([transfer_qty_to_pop, row.posting_date, stock_value])
|
||||
transfer_qty_to_pop = 0
|
||||
stock_value = 0
|
||||
else:
|
||||
# ample bucket qty to consume
|
||||
transfer_data[0][0] -= transfer_qty_to_pop
|
||||
add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]])
|
||||
transfer_data[0][2] -= stock_value
|
||||
add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1], stock_value])
|
||||
transfer_qty_to_pop = 0
|
||||
stock_value = 0
|
||||
|
||||
def __update_balances(self, row: dict, key: tuple | str):
|
||||
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction
|
||||
@@ -412,6 +437,7 @@ class FIFOSlots:
|
||||
item.stock_uom,
|
||||
item.has_serial_no,
|
||||
sle.actual_qty,
|
||||
sle.stock_value_difference,
|
||||
sle.posting_date,
|
||||
sle.voucher_type,
|
||||
sle.voucher_no,
|
||||
|
||||
@@ -18,6 +18,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=30,
|
||||
qty_after_transaction=30,
|
||||
stock_value_difference=30,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -29,6 +30,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=20,
|
||||
qty_after_transaction=50,
|
||||
stock_value_difference=20,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-02",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -40,6 +42,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-10),
|
||||
qty_after_transaction=40,
|
||||
stock_value_difference=(-10),
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -57,6 +60,8 @@ class TestStockAgeing(FrappeTestCase):
|
||||
|
||||
self.assertEqual(result["qty_after_transaction"], result["total_qty"])
|
||||
self.assertEqual(queue[0][0], 20.0)
|
||||
data = format_report_data(self.filters, slots, self.filters["to_date"])
|
||||
self.assertEqual(data[0][8], 40.0) # valuating for stock value between age 0-30
|
||||
|
||||
def test_insufficient_balance(self):
|
||||
"Reference: Case 3 in stock_ageing_fifo_logic.md (same wh)"
|
||||
@@ -65,6 +70,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-30),
|
||||
qty_after_transaction=(-30),
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -76,6 +82,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=20,
|
||||
qty_after_transaction=(-10),
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-02",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -87,6 +94,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=20,
|
||||
qty_after_transaction=10,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -98,6 +106,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=10,
|
||||
qty_after_transaction=20,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -126,6 +135,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=30,
|
||||
qty_after_transaction=30,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -137,6 +147,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=0,
|
||||
qty_after_transaction=50,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-02",
|
||||
voucher_type="Stock Reconciliation",
|
||||
@@ -148,6 +159,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-10),
|
||||
qty_after_transaction=40,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -178,6 +190,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=0,
|
||||
qty_after_transaction=1000,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Reconciliation",
|
||||
@@ -189,6 +202,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=0,
|
||||
qty_after_transaction=400,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-02",
|
||||
voucher_type="Stock Reconciliation",
|
||||
@@ -200,6 +214,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-10),
|
||||
qty_after_transaction=390,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -233,6 +248,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=0,
|
||||
qty_after_transaction=1000,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Reconciliation",
|
||||
@@ -244,6 +260,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=0,
|
||||
qty_after_transaction=400,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 2",
|
||||
posting_date="2021-12-02",
|
||||
voucher_type="Stock Reconciliation",
|
||||
@@ -255,6 +272,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-10),
|
||||
qty_after_transaction=990,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -301,6 +319,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=500,
|
||||
qty_after_transaction=500,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -312,6 +331,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-50),
|
||||
qty_after_transaction=450,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -323,6 +343,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-50),
|
||||
qty_after_transaction=400,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -334,6 +355,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=100,
|
||||
qty_after_transaction=500,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -370,6 +392,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=500,
|
||||
qty_after_transaction=500,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -381,6 +404,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-100),
|
||||
qty_after_transaction=400,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -392,6 +416,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=50,
|
||||
qty_after_transaction=450,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -426,6 +451,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=20,
|
||||
qty_after_transaction=20,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -437,6 +463,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-50),
|
||||
qty_after_transaction=(-30),
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -448,6 +475,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-50),
|
||||
qty_after_transaction=(-80),
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -459,6 +487,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=50,
|
||||
qty_after_transaction=(-30),
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -496,6 +525,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=500,
|
||||
qty_after_transaction=500,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -507,6 +537,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-50),
|
||||
qty_after_transaction=450,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -518,6 +549,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=100,
|
||||
qty_after_transaction=550,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -553,6 +585,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=20,
|
||||
qty_after_transaction=20,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -564,6 +597,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-50),
|
||||
qty_after_transaction=(-30),
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -575,6 +609,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=50,
|
||||
qty_after_transaction=20,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -586,6 +621,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=50,
|
||||
qty_after_transaction=70,
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-04",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -623,6 +659,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-50),
|
||||
qty_after_transaction=(-50),
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -634,6 +671,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=(-50),
|
||||
qty_after_transaction=(-100),
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -645,6 +683,7 @@ class TestStockAgeing(FrappeTestCase):
|
||||
name="Flask Item",
|
||||
actual_qty=30,
|
||||
qty_after_transaction=(-70),
|
||||
stock_value_difference=0,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
@@ -722,6 +761,113 @@ class TestStockAgeing(FrappeTestCase):
|
||||
self.assertEqual(bal_qty, 0.9)
|
||||
self.assertEqual(bal_qty, range_qty_sum)
|
||||
|
||||
def test_ageing_stock_valuation(self):
|
||||
"Test stock valuation for each time bucket."
|
||||
sle = [
|
||||
frappe._dict(
|
||||
name="Flask Item",
|
||||
actual_qty=10,
|
||||
qty_after_transaction=10,
|
||||
stock_value_difference=10,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="001",
|
||||
has_serial_no=False,
|
||||
serial_no=None,
|
||||
),
|
||||
frappe._dict(
|
||||
name="Flask Item",
|
||||
actual_qty=20,
|
||||
qty_after_transaction=30,
|
||||
stock_value_difference=20,
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-02",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="002",
|
||||
has_serial_no=False,
|
||||
serial_no=None,
|
||||
),
|
||||
frappe._dict(
|
||||
name="Flask Item",
|
||||
actual_qty=(-10),
|
||||
qty_after_transaction=20,
|
||||
stock_value_difference=(-10),
|
||||
warehouse="WH 1",
|
||||
posting_date="2021-12-03",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="003",
|
||||
has_serial_no=False,
|
||||
serial_no=None,
|
||||
),
|
||||
frappe._dict(
|
||||
name="Flask Item",
|
||||
actual_qty=10,
|
||||
qty_after_transaction=30,
|
||||
stock_value_difference=20,
|
||||
warehouse="WH 1",
|
||||
posting_date="2022-01-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="004",
|
||||
has_serial_no=False,
|
||||
serial_no=None,
|
||||
),
|
||||
frappe._dict(
|
||||
name="Flask Item",
|
||||
actual_qty=(-15),
|
||||
qty_after_transaction=15,
|
||||
stock_value_difference=(-15),
|
||||
warehouse="WH 1",
|
||||
posting_date="2022-01-02",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="005",
|
||||
has_serial_no=False,
|
||||
serial_no=None,
|
||||
),
|
||||
frappe._dict(
|
||||
name="Flask Item",
|
||||
actual_qty=10,
|
||||
qty_after_transaction=25,
|
||||
stock_value_difference=5,
|
||||
warehouse="WH 1",
|
||||
posting_date="2022-02-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="006",
|
||||
has_serial_no=False,
|
||||
serial_no=None,
|
||||
),
|
||||
frappe._dict(
|
||||
name="Flask Item",
|
||||
actual_qty=5,
|
||||
qty_after_transaction=30,
|
||||
stock_value_difference=2.5,
|
||||
warehouse="WH 1",
|
||||
posting_date="2022-02-02",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="007",
|
||||
has_serial_no=False,
|
||||
serial_no=None,
|
||||
),
|
||||
frappe._dict(
|
||||
name="Flask Item",
|
||||
actual_qty=5,
|
||||
qty_after_transaction=35,
|
||||
stock_value_difference=15,
|
||||
warehouse="WH 1",
|
||||
posting_date="2022-03-01",
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no="008",
|
||||
has_serial_no=False,
|
||||
serial_no=None,
|
||||
),
|
||||
]
|
||||
|
||||
slots = FIFOSlots(self.filters, sle).generate()
|
||||
report_data = format_report_data(self.filters, slots, "2022-03-31")
|
||||
range_values = report_data[0][7:15]
|
||||
range_valuations = range_values[1::2]
|
||||
self.assertEqual(range_valuations, [15, 7.5, 20, 5])
|
||||
|
||||
|
||||
def generate_item_and_item_wh_wise_slots(filters, sle):
|
||||
"Return results with and without 'show_warehouse_wise_stock'"
|
||||
|
||||
@@ -351,6 +351,15 @@ class SerialBatchBundle:
|
||||
status = "Inactive"
|
||||
if self.sle.actual_qty < 0:
|
||||
status = "Delivered"
|
||||
if self.sle.voucher_type == "Stock Entry":
|
||||
purpose = frappe.get_cached_value("Stock Entry", self.sle.voucher_no, "purpose")
|
||||
if purpose in [
|
||||
"Manufacture",
|
||||
"Material Issue",
|
||||
"Repack",
|
||||
"Material Consumption for Manufacture",
|
||||
]:
|
||||
status = "Consumed"
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
|
||||
|
||||
@@ -622,15 +622,12 @@ class update_entries_after:
|
||||
if sle.dependant_sle_voucher_detail_no:
|
||||
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
|
||||
|
||||
if self.has_stock_reco_with_serial_batch(sle):
|
||||
break
|
||||
|
||||
if self.exceptions:
|
||||
self.raise_exceptions()
|
||||
|
||||
def has_stock_reco_with_serial_batch(self, sle):
|
||||
if (
|
||||
sle.vocher_type == "Stock Reconciliation"
|
||||
sle.voucher_type == "Stock Reconciliation"
|
||||
and frappe.db.get_value(sle.voucher_type, sle.voucher_no, "set_posting_time") == 1
|
||||
):
|
||||
return not (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
|
||||
|
||||
Reference in New Issue
Block a user