mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-29 18:04:46 +00:00
Merge pull request #41355 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -1102,7 +1102,7 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
so.load_from_db()
|
so.load_from_db()
|
||||||
self.assertEqual(so.items[1].is_free_item, 1)
|
self.assertEqual(so.items[1].is_free_item, 1)
|
||||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||||
self.assertEqual(so.items[1].qty, 4)
|
self.assertEqual(so.items[1].qty, 3)
|
||||||
|
|
||||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold
|
from frappe import _, bold
|
||||||
@@ -653,7 +654,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
|||||||
if transaction_qty:
|
if transaction_qty:
|
||||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||||
if pricing_rule.round_free_qty:
|
if pricing_rule.round_free_qty:
|
||||||
qty = round(qty)
|
qty = math.floor(qty)
|
||||||
|
|
||||||
free_item_data_args = {
|
free_item_data_args = {
|
||||||
"item_code": free_item,
|
"item_code": free_item,
|
||||||
|
|||||||
@@ -11,13 +11,15 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "cost_center_name",
|
"fieldname": "cost_center_name",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-08-03 16:56:45.744905",
|
"modified": "2024-05-03 17:16:51.666461",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "PSOA Cost Center",
|
"name": "PSOA Cost Center",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class PSOACostCenter(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
cost_center_name: DF.Link | None
|
cost_center_name: DF.Link
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
|||||||
@@ -1091,7 +1091,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# check if the exchange rate has changed
|
# check if the exchange rate has changed
|
||||||
if item.get("purchase_receipt"):
|
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
|
||||||
if (
|
if (
|
||||||
exchange_rate_map[item.purchase_receipt]
|
exchange_rate_map[item.purchase_receipt]
|
||||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||||
|
|||||||
@@ -388,6 +388,9 @@ class SalesInvoice(SellingController):
|
|||||||
validate_account_head(item.idx, item.income_account, self.company, "Income")
|
validate_account_head(item.idx, item.income_account, self.company, "Income")
|
||||||
|
|
||||||
def set_tax_withholding(self):
|
def set_tax_withholding(self):
|
||||||
|
if self.get("is_opening") == "Yes":
|
||||||
|
return
|
||||||
|
|
||||||
tax_withholding_details = get_party_tax_withholding_details(self)
|
tax_withholding_details = get_party_tax_withholding_details(self)
|
||||||
|
|
||||||
if not tax_withholding_details:
|
if not tax_withholding_details:
|
||||||
|
|||||||
@@ -1766,6 +1766,49 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertTrue(gle)
|
self.assertTrue(gle)
|
||||||
|
|
||||||
|
def test_gle_in_transaction_currency(self):
|
||||||
|
# create multi currency sales invoice with 2 items with same income account
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer="_Test Customer USD",
|
||||||
|
debit_to="_Test Receivable USD - _TC",
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=50,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
# add 2nd item with same income account
|
||||||
|
si.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 80,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
gl_entries = frappe.db.sql(
|
||||||
|
"""select transaction_currency, transaction_exchange_rate,
|
||||||
|
debit_in_transaction_currency, credit_in_transaction_currency
|
||||||
|
from `tabGL Entry`
|
||||||
|
where voucher_type='Sales Invoice' and voucher_no=%s and account = 'Sales - _TC'
|
||||||
|
order by account asc""",
|
||||||
|
si.name,
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_gle = {
|
||||||
|
"transaction_currency": "USD",
|
||||||
|
"transaction_exchange_rate": 50,
|
||||||
|
"debit_in_transaction_currency": 0,
|
||||||
|
"credit_in_transaction_currency": 180,
|
||||||
|
}
|
||||||
|
|
||||||
|
for gle in gl_entries:
|
||||||
|
for field in expected_gle:
|
||||||
|
self.assertEqual(expected_gle[field], gle[field])
|
||||||
|
|
||||||
def test_invoice_exchange_rate(self):
|
def test_invoice_exchange_rate(self):
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
customer="_Test Customer USD",
|
customer="_Test Customer USD",
|
||||||
|
|||||||
@@ -112,11 +112,7 @@ class Subscription(Document):
|
|||||||
"""
|
"""
|
||||||
_current_invoice_start = None
|
_current_invoice_start = None
|
||||||
|
|
||||||
if (
|
if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
|
||||||
self.is_new_subscription()
|
|
||||||
and self.trial_period_end
|
|
||||||
and getdate(self.trial_period_end) > getdate(self.start_date)
|
|
||||||
):
|
|
||||||
_current_invoice_start = add_days(self.trial_period_end, 1)
|
_current_invoice_start = add_days(self.trial_period_end, 1)
|
||||||
elif self.trial_period_start and self.is_trialling():
|
elif self.trial_period_start and self.is_trialling():
|
||||||
_current_invoice_start = self.trial_period_start
|
_current_invoice_start = self.trial_period_start
|
||||||
@@ -143,7 +139,7 @@ class Subscription(Document):
|
|||||||
else:
|
else:
|
||||||
billing_cycle_info = self.get_billing_cycle_data()
|
billing_cycle_info = self.get_billing_cycle_data()
|
||||||
if billing_cycle_info:
|
if billing_cycle_info:
|
||||||
if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
|
if getdate(self.start_date) < getdate(date):
|
||||||
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
||||||
|
|
||||||
# For cases where trial period is for an entire billing interval
|
# For cases where trial period is for an entire billing interval
|
||||||
@@ -234,14 +230,14 @@ class Subscription(Document):
|
|||||||
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
|
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
|
||||||
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
||||||
self.status = "Past Due Date"
|
self.status = "Past Due Date"
|
||||||
elif not self.has_outstanding_invoice() or self.is_new_subscription():
|
elif not self.has_outstanding_invoice():
|
||||||
self.status = "Active"
|
self.status = "Active"
|
||||||
|
|
||||||
def is_trialling(self) -> bool:
|
def is_trialling(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns `True` if the `Subscription` is in trial period.
|
Returns `True` if the `Subscription` is in trial period.
|
||||||
"""
|
"""
|
||||||
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
|
return not self.period_has_passed(self.trial_period_end)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def period_has_passed(
|
def period_has_passed(
|
||||||
@@ -288,14 +284,6 @@ class Subscription(Document):
|
|||||||
def invoice_document_type(self) -> str:
|
def invoice_document_type(self) -> str:
|
||||||
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||||
|
|
||||||
def is_new_subscription(self) -> bool:
|
|
||||||
"""
|
|
||||||
Returns `True` if `Subscription` has never generated an invoice
|
|
||||||
"""
|
|
||||||
return self.is_new() or not frappe.db.exists(
|
|
||||||
{"doctype": self.invoice_document_type, "subscription": self.name}
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate(self) -> None:
|
def validate(self) -> None:
|
||||||
self.validate_trial_period()
|
self.validate_trial_period()
|
||||||
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
||||||
@@ -604,7 +592,7 @@ class Subscription(Document):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if self.generate_invoice_at == "Beginning of the current subscription period" and (
|
if self.generate_invoice_at == "Beginning of the current subscription period" and (
|
||||||
getdate(posting_date) == getdate(self.current_invoice_start) or self.is_new_subscription()
|
getdate(posting_date) == getdate(self.current_invoice_start)
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
elif self.generate_invoice_at == "Days before the current subscription period" and (
|
elif self.generate_invoice_at == "Days before the current subscription period" and (
|
||||||
|
|||||||
@@ -445,11 +445,11 @@ class TestSubscription(FrappeTestCase):
|
|||||||
|
|
||||||
# Process subscription and create first invoice
|
# Process subscription and create first invoice
|
||||||
# Subscription status will be unpaid since due date has already passed
|
# Subscription status will be unpaid since due date has already passed
|
||||||
subscription.process()
|
subscription.process(posting_date="2018-01-01")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
|
|
||||||
subscription.process()
|
subscription.process(posting_date="2018-04-01")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
|
||||||
def test_multi_currency_subscription(self):
|
def test_multi_currency_subscription(self):
|
||||||
@@ -462,7 +462,7 @@ class TestSubscription(FrappeTestCase):
|
|||||||
party=party,
|
party=party,
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription.process()
|
subscription.process(posting_date="2018-01-01")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from frappe.query_builder import Criterion
|
|||||||
from frappe.query_builder.functions import Abs, Sum
|
from frappe.query_builder.functions import Abs, Sum
|
||||||
from frappe.utils import cint, flt, getdate
|
from frappe.utils import cint, flt, getdate
|
||||||
|
|
||||||
|
from erpnext.controllers.accounts_controller import validate_account_head
|
||||||
|
|
||||||
|
|
||||||
class TaxWithholdingCategory(Document):
|
class TaxWithholdingCategory(Document):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
@@ -53,6 +55,7 @@ class TaxWithholdingCategory(Document):
|
|||||||
if d.get("account") in existing_accounts:
|
if d.get("account") in existing_accounts:
|
||||||
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get("account"))))
|
||||||
|
|
||||||
|
validate_account_head(d.idx, d.get("account"), d.get("company"))
|
||||||
existing_accounts.append(d.get("account"))
|
existing_accounts.append(d.get("account"))
|
||||||
|
|
||||||
def validate_thresholds(self):
|
def validate_thresholds(self):
|
||||||
|
|||||||
@@ -238,10 +238,16 @@ def merge_similar_entries(gl_map, precision=None):
|
|||||||
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
|
||||||
entry.debit_in_account_currency
|
entry.debit_in_account_currency
|
||||||
)
|
)
|
||||||
|
same_head.debit_in_transaction_currency = flt(same_head.debit_in_transaction_currency) + flt(
|
||||||
|
entry.debit_in_transaction_currency
|
||||||
|
)
|
||||||
same_head.credit = flt(same_head.credit) + flt(entry.credit)
|
same_head.credit = flt(same_head.credit) + flt(entry.credit)
|
||||||
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
|
same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
|
||||||
entry.credit_in_account_currency
|
entry.credit_in_account_currency
|
||||||
)
|
)
|
||||||
|
same_head.credit_in_transaction_currency = flt(same_head.credit_in_transaction_currency) + flt(
|
||||||
|
entry.credit_in_transaction_currency
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
merged_gl_map.append(entry)
|
merged_gl_map.append(entry)
|
||||||
|
|
||||||
|
|||||||
@@ -720,20 +720,22 @@ class GrossProfitGenerator:
|
|||||||
frappe.qb.from_(purchase_invoice_item)
|
frappe.qb.from_(purchase_invoice_item)
|
||||||
.inner_join(purchase_invoice)
|
.inner_join(purchase_invoice)
|
||||||
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
||||||
.select(purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor)
|
.select(
|
||||||
|
purchase_invoice.name,
|
||||||
|
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
|
||||||
|
)
|
||||||
.where(purchase_invoice.docstatus == 1)
|
.where(purchase_invoice.docstatus == 1)
|
||||||
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
||||||
.where(purchase_invoice_item.item_code == item_code)
|
.where(purchase_invoice_item.item_code == item_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
if row.project:
|
if row.project:
|
||||||
query.where(purchase_invoice_item.project == row.project)
|
query = query.where(purchase_invoice_item.project == row.project)
|
||||||
|
|
||||||
if row.cost_center:
|
if row.cost_center:
|
||||||
query.where(purchase_invoice_item.cost_center == row.cost_center)
|
query = query.where(purchase_invoice_item.cost_center == row.cost_center)
|
||||||
|
|
||||||
query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
|
query = query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc).limit(1)
|
||||||
query.limit(1)
|
|
||||||
last_purchase_rate = query.run()
|
last_purchase_rate = query.run()
|
||||||
|
|
||||||
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
||||||
|
|||||||
@@ -143,6 +143,10 @@ class AssetCapitalization(StockController):
|
|||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.restore_consumed_asset_items()
|
self.restore_consumed_asset_items()
|
||||||
|
|
||||||
|
def on_trash(self):
|
||||||
|
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
||||||
|
super(AssetCapitalization, self).on_trash()
|
||||||
|
|
||||||
def cancel_target_asset(self):
|
def cancel_target_asset(self):
|
||||||
if self.entry_type == "Capitalization" and self.target_asset:
|
if self.entry_type == "Capitalization" and self.target_asset:
|
||||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||||
|
|||||||
@@ -612,6 +612,20 @@ class PurchaseOrder(BuyingController):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def update_ordered_qty_in_so_for_removed_items(self, removed_items):
|
||||||
|
"""
|
||||||
|
Updates ordered_qty in linked SO when item rows are removed using Update Items
|
||||||
|
"""
|
||||||
|
if not self.is_against_so():
|
||||||
|
return
|
||||||
|
for item in removed_items:
|
||||||
|
prev_ordered_qty = frappe.get_cached_value(
|
||||||
|
"Sales Order Item", item.get("sales_order_item"), "ordered_qty"
|
||||||
|
)
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty
|
||||||
|
)
|
||||||
|
|
||||||
def auto_create_subcontracting_order(self):
|
def auto_create_subcontracting_order(self):
|
||||||
if self.is_subcontracted and not self.is_old_subcontracting_flow:
|
if self.is_subcontracted and not self.is_old_subcontracting_flow:
|
||||||
if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"):
|
if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"):
|
||||||
|
|||||||
@@ -3158,6 +3158,9 @@ def validate_and_delete_children(parent, data) -> bool:
|
|||||||
d.cancel()
|
d.cancel()
|
||||||
d.delete()
|
d.delete()
|
||||||
|
|
||||||
|
if parent.doctype == "Purchase Order":
|
||||||
|
parent.update_ordered_qty_in_so_for_removed_items(deleted_children)
|
||||||
|
|
||||||
# need to update ordered qty in Material Request first
|
# need to update ordered qty in Material Request first
|
||||||
# bin uses Material Request Items to recalculate & update
|
# bin uses Material Request Items to recalculate & update
|
||||||
parent.update_prevdoc_status()
|
parent.update_prevdoc_status()
|
||||||
|
|||||||
@@ -361,3 +361,5 @@ erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
|||||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||||
|
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||||
|
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
# update debit and credit in transaction currency:
|
||||||
|
# if transaction currency is same as account currency,
|
||||||
|
# then debit and credit in transaction currency is same as debit and credit in account currency
|
||||||
|
# else debit and credit divided by exchange rate
|
||||||
|
|
||||||
|
# nosemgrep
|
||||||
|
frappe.db.sql(
|
||||||
|
"""
|
||||||
|
UPDATE `tabGL Entry`
|
||||||
|
SET
|
||||||
|
debit_in_transaction_currency = IF(transaction_currency = account_currency, debit_in_account_currency, debit / transaction_exchange_rate),
|
||||||
|
credit_in_transaction_currency = IF(transaction_currency = account_currency, credit_in_account_currency, credit / transaction_exchange_rate)
|
||||||
|
WHERE
|
||||||
|
transaction_exchange_rate > 0
|
||||||
|
and transaction_currency is not null
|
||||||
|
"""
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
cancelled_asset_capitalizations = frappe.get_all(
|
||||||
|
"Asset Capitalization",
|
||||||
|
filters={"docstatus": 2},
|
||||||
|
fields=["name", "target_asset"],
|
||||||
|
)
|
||||||
|
for asset_capitalization in cancelled_asset_capitalizations:
|
||||||
|
frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None)
|
||||||
@@ -15,6 +15,9 @@ frappe.ui.form.on("Item", {
|
|||||||
frm.add_fetch("tax_type", "tax_rate", "tax_rate");
|
frm.add_fetch("tax_type", "tax_rate", "tax_rate");
|
||||||
|
|
||||||
frm.make_methods = {
|
frm.make_methods = {
|
||||||
|
Quotation: () => {
|
||||||
|
open_form(frm, "Quotation", "Quotation Item", "items");
|
||||||
|
},
|
||||||
"Sales Order": () => {
|
"Sales Order": () => {
|
||||||
open_form(frm, "Sales Order", "Sales Order Item", "items");
|
open_form(frm, "Sales Order", "Sales Order Item", "items");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,6 +36,8 @@
|
|||||||
"section_break_11",
|
"section_break_11",
|
||||||
"description",
|
"description",
|
||||||
"brand",
|
"brand",
|
||||||
|
"unit_of_measure_conversion",
|
||||||
|
"uoms",
|
||||||
"dashboard_tab",
|
"dashboard_tab",
|
||||||
"inventory_section",
|
"inventory_section",
|
||||||
"inventory_settings_section",
|
"inventory_settings_section",
|
||||||
@@ -52,8 +54,6 @@
|
|||||||
"barcodes",
|
"barcodes",
|
||||||
"reorder_section",
|
"reorder_section",
|
||||||
"reorder_levels",
|
"reorder_levels",
|
||||||
"unit_of_measure_conversion",
|
|
||||||
"uoms",
|
|
||||||
"serial_nos_and_batches",
|
"serial_nos_and_batches",
|
||||||
"has_batch_no",
|
"has_batch_no",
|
||||||
"create_new_batch",
|
"create_new_batch",
|
||||||
@@ -891,7 +891,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-01-08 18:09:30.225085",
|
"modified": "2024-04-30 13:46:39.098753",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item",
|
"name": "Item",
|
||||||
|
|||||||
@@ -65,15 +65,13 @@ class Item(Document):
|
|||||||
from erpnext.stock.doctype.item_reorder.item_reorder import ItemReorder
|
from erpnext.stock.doctype.item_reorder.item_reorder import ItemReorder
|
||||||
from erpnext.stock.doctype.item_supplier.item_supplier import ItemSupplier
|
from erpnext.stock.doctype.item_supplier.item_supplier import ItemSupplier
|
||||||
from erpnext.stock.doctype.item_tax.item_tax import ItemTax
|
from erpnext.stock.doctype.item_tax.item_tax import ItemTax
|
||||||
from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import (
|
from erpnext.stock.doctype.item_variant_attribute.item_variant_attribute import ItemVariantAttribute
|
||||||
ItemVariantAttribute,
|
|
||||||
)
|
|
||||||
from erpnext.stock.doctype.uom_conversion_detail.uom_conversion_detail import UOMConversionDetail
|
from erpnext.stock.doctype.uom_conversion_detail.uom_conversion_detail import UOMConversionDetail
|
||||||
|
|
||||||
allow_alternative_item: DF.Check
|
allow_alternative_item: DF.Check
|
||||||
allow_negative_stock: DF.Check
|
allow_negative_stock: DF.Check
|
||||||
asset_category: DF.Link | None
|
asset_category: DF.Link | None
|
||||||
asset_naming_series: DF.Literal
|
asset_naming_series: DF.Literal[None]
|
||||||
attributes: DF.Table[ItemVariantAttribute]
|
attributes: DF.Table[ItemVariantAttribute]
|
||||||
auto_create_assets: DF.Check
|
auto_create_assets: DF.Check
|
||||||
barcodes: DF.Table[ItemBarcode]
|
barcodes: DF.Table[ItemBarcode]
|
||||||
|
|||||||
@@ -52,10 +52,13 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "item_code.stock_uom",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "uom",
|
"fieldname": "uom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "UOM",
|
"label": "UOM",
|
||||||
"options": "UOM"
|
"options": "UOM",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -220,7 +223,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-30 14:02:19.304854",
|
"modified": "2024-04-02 22:18:00.450641",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item Price",
|
"name": "Item Price",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class ItemPrice(Document):
|
|||||||
reference: DF.Data | None
|
reference: DF.Data | None
|
||||||
selling: DF.Check
|
selling: DF.Check
|
||||||
supplier: DF.Link | None
|
supplier: DF.Link | None
|
||||||
uom: DF.Link | None
|
uom: DF.Link
|
||||||
valid_from: DF.Date | None
|
valid_from: DF.Date | None
|
||||||
valid_upto: DF.Date | None
|
valid_upto: DF.Date | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|||||||
@@ -845,6 +845,7 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list
|
|||||||
|
|
||||||
picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0)
|
picked_qty = picked_item_details.get(key, {}).get("picked_qty", 0)
|
||||||
if not picked_qty:
|
if not picked_qty:
|
||||||
|
filterd_locations.append(row)
|
||||||
continue
|
continue
|
||||||
if picked_qty > row.qty:
|
if picked_qty > row.qty:
|
||||||
row.qty = 0
|
row.qty = 0
|
||||||
|
|||||||
@@ -1013,3 +1013,121 @@ class TestPickList(FrappeTestCase):
|
|||||||
pl.submit()
|
pl.submit()
|
||||||
self.assertEqual(pl.locations[0].qty, 4.0)
|
self.assertEqual(pl.locations[0].qty, 4.0)
|
||||||
self.assertTrue(hasattr(pl, "locations"))
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
def test_pick_list_for_multiple_sales_order_with_multiple_batches(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Batch Pick List Item For Multiple Batches and Sales Order",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"batch_number_series": "SN-SOO-BT-SPLIMBATCH-.####",
|
||||||
|
"create_new_batch": 1,
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||||
|
|
||||||
|
pl1 = create_pick_list(so.name)
|
||||||
|
pl1.save()
|
||||||
|
self.assertEqual(pl1.locations[0].qty, 10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 90.0)
|
||||||
|
self.assertEqual(pl.locations[1].qty, 20.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
pl1.submit()
|
||||||
|
|
||||||
|
pl.reload()
|
||||||
|
pl.submit()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 90.0)
|
||||||
|
self.assertEqual(pl.locations[1].qty, 20.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
def test_pick_list_for_multiple_sales_order_with_multiple_serial_nos(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Serial No Pick List Item For Multiple Batches and Sales Order",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "SNNN-SOO-BT-SPLIMBATCH-.####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||||
|
|
||||||
|
pl1 = create_pick_list(so.name)
|
||||||
|
pl1.save()
|
||||||
|
self.assertEqual(pl1.locations[0].qty, 10)
|
||||||
|
|
||||||
|
serial_nos = pl1.locations[0].serial_no.split("\n")
|
||||||
|
self.assertEqual(len(serial_nos), 10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
new_serial_nos = pl.locations[0].serial_no.split("\n")
|
||||||
|
self.assertEqual(len(new_serial_nos), 110)
|
||||||
|
|
||||||
|
for sn in serial_nos:
|
||||||
|
self.assertFalse(sn in new_serial_nos)
|
||||||
|
|
||||||
|
pl1.submit()
|
||||||
|
|
||||||
|
pl.reload()
|
||||||
|
pl.submit()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
def test_pick_list_for_multiple_sales_orders_for_non_serialized_item(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Non Serialized Pick List Item For Multiple Batches and Sales Order",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||||
|
|
||||||
|
pl1 = create_pick_list(so.name)
|
||||||
|
pl1.save()
|
||||||
|
self.assertEqual(pl1.locations[0].qty, 10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||||
|
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
pl1.submit()
|
||||||
|
|
||||||
|
pl.reload()
|
||||||
|
pl.submit()
|
||||||
|
self.assertEqual(pl.locations[0].qty, 110.0)
|
||||||
|
self.assertTrue(hasattr(pl, "locations"))
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=110, rate=100)
|
||||||
|
pl = create_pick_list(so.name)
|
||||||
|
pl.save()
|
||||||
|
|
||||||
|
self.assertEqual(pl.locations[0].qty, 80.0)
|
||||||
|
|||||||
@@ -132,7 +132,8 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Item",
|
"label": "Item",
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "quantity_section",
|
"fieldname": "quantity_section",
|
||||||
@@ -240,7 +241,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-02-04 16:12:16.257951",
|
"modified": "2024-05-07 15:32:42.905446",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List Item",
|
"name": "Pick List Item",
|
||||||
|
|||||||
@@ -838,7 +838,12 @@ def insert_item_price(args):
|
|||||||
|
|
||||||
item_price = frappe.db.get_value(
|
item_price = frappe.db.get_value(
|
||||||
"Item Price",
|
"Item Price",
|
||||||
{"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency},
|
{
|
||||||
|
"item_code": args.item_code,
|
||||||
|
"price_list": args.price_list,
|
||||||
|
"currency": args.currency,
|
||||||
|
"uom": args.stock_uom,
|
||||||
|
},
|
||||||
["name", "price_list_rate"],
|
["name", "price_list_rate"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -30,8 +30,15 @@ def execute(filters=None):
|
|||||||
|
|
||||||
sle_count = _estimate_table_row_count("Stock Ledger Entry")
|
sle_count = _estimate_table_row_count("Stock Ledger Entry")
|
||||||
|
|
||||||
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
|
if (
|
||||||
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
|
sle_count > SLE_COUNT_LIMIT
|
||||||
|
and not filters.get("item_code")
|
||||||
|
and not filters.get("warehouse")
|
||||||
|
and not filters.get("warehouse_type")
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("Please select either the Item or Warehouse or Warehouse Type filter to generate the report.")
|
||||||
|
)
|
||||||
|
|
||||||
if filters.from_date > filters.to_date:
|
if filters.from_date > filters.to_date:
|
||||||
frappe.throw(_("From Date must be before To Date"))
|
frappe.throw(_("From Date must be before To Date"))
|
||||||
|
|||||||
@@ -67,3 +67,9 @@ typing-modules = ["frappe.types.DF"]
|
|||||||
quote-style = "double"
|
quote-style = "double"
|
||||||
indent-style = "tab"
|
indent-style = "tab"
|
||||||
docstring-code-format = true
|
docstring-code-format = true
|
||||||
|
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://erpnext.com/"
|
||||||
|
Repository = "https://github.com/frappe/erpnext.git"
|
||||||
|
"Bug Reports" = "https://github.com/frappe/erpnext/issues"
|
||||||
|
|||||||
Reference in New Issue
Block a user