mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-04 18:23:05 +00:00
Compare commits
24 Commits
version-15
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d738cbbd4 | ||
|
|
7098602dcc | ||
|
|
32b56ac505 | ||
|
|
9f1bdba9a7 | ||
|
|
c630226846 | ||
|
|
d5b49cd66e | ||
|
|
b9c9b76f5b | ||
|
|
da0e3b5882 | ||
|
|
3d4198494b | ||
|
|
e6b4799b1a | ||
|
|
49eb2366cd | ||
|
|
fb2a4e5f98 | ||
|
|
0ce7917648 | ||
|
|
41038979ec | ||
|
|
88b6779062 | ||
|
|
b5f784612d | ||
|
|
457424f7a4 | ||
|
|
25ee3695f0 | ||
|
|
ff205da810 | ||
|
|
2980171007 | ||
|
|
c6c4815e8d | ||
|
|
82a85818c2 | ||
|
|
57a2be6b56 | ||
|
|
47f54a4725 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.112.0"
|
||||
__version__ = "15.118.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -3316,11 +3316,13 @@ def set_paid_amount_and_received_amount(
|
||||
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
|
||||
if bank and company_currency != bank.account_currency:
|
||||
# doc currency can be different from bank currency
|
||||
conversion_rate = get_exchange_rate(bank.account_currency, party_account_currency)
|
||||
posting_date = doc.get("posting_date") or doc.get("transaction_date")
|
||||
conversion_rate = get_exchange_rate(
|
||||
bank.account_currency, party_account_currency, posting_date
|
||||
)
|
||||
received_amount = paid_amount / conversion_rate
|
||||
else:
|
||||
conversion_rate = get_exchange_rate(doc.get("currency", company_currency), company_currency)
|
||||
received_amount = paid_amount * conversion_rate
|
||||
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
||||
|
||||
# if payment type is pay, then paid amount and received amount are swapped
|
||||
if payment_type == "Pay":
|
||||
|
||||
@@ -33,8 +33,6 @@ frappe.ui.form.on("Payment Request", "onload", function (frm, dt, dn) {
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
||||
let sending_email = false;
|
||||
|
||||
if (
|
||||
frm.doc.payment_request_type == "Inward" &&
|
||||
frm.doc.payment_channel !== "Phone" &&
|
||||
@@ -43,16 +41,16 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
||||
frm.doc.docstatus == 1
|
||||
) {
|
||||
frm.add_custom_button(__("Resend Payment Email"), function () {
|
||||
if (sending_email) {
|
||||
frappe.show_alert({ message: __("Sending Email"), indicator: "blue" });
|
||||
return;
|
||||
}
|
||||
sending_email = true;
|
||||
frappe.show_alert({ message: __("Sending Email"), indicator: "blue" });
|
||||
frm.call("resend_payment_email").then((r) => {
|
||||
const msg = !r.exc ? __("Email Sent") : __("Email couldn't be sent.");
|
||||
frappe.show_alert({ message: msg, indicator: !r.exc ? "green" : "red" });
|
||||
sending_email = false;
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_request.payment_request.resend_payment_email",
|
||||
args: { docname: frm.doc.name },
|
||||
freeze: true,
|
||||
freeze_message: __("Sending"),
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__("Message Sent"));
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -411,18 +411,6 @@ class PaymentRequest(Document):
|
||||
|
||||
return payment_entry
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def resend_payment_email(self):
|
||||
if not (
|
||||
self.docstatus == 1
|
||||
and self.payment_request_type == "Inward"
|
||||
and self.payment_channel != "Phone"
|
||||
and self.status not in ["Initiated", "Paid"]
|
||||
):
|
||||
frappe.throw(_("Payment Link couldn't be sent."))
|
||||
|
||||
self.send_email()
|
||||
|
||||
def send_email(self):
|
||||
"""send email with payment link"""
|
||||
email_args = {
|
||||
@@ -440,17 +428,7 @@ class PaymentRequest(Document):
|
||||
)
|
||||
],
|
||||
}
|
||||
job_id = f"send_payment_email::{self.name}"
|
||||
enqueue(
|
||||
method=frappe.sendmail,
|
||||
queue="short",
|
||||
timeout=300,
|
||||
is_async=True,
|
||||
job_id=job_id,
|
||||
deduplicate=True,
|
||||
enqueue_after_commit=True,
|
||||
**email_args,
|
||||
)
|
||||
enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
|
||||
|
||||
def get_message(self):
|
||||
"""return message with payment gateway link"""
|
||||
@@ -849,6 +827,11 @@ def get_print_format_list(ref_doctype):
|
||||
return {"print_format": print_format_list}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def resend_payment_email(docname):
|
||||
return frappe.get_doc("Payment Request", docname).send_email()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_payment_entry(docname):
|
||||
doc = frappe.get_doc("Payment Request", docname)
|
||||
|
||||
@@ -15,7 +15,6 @@ 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.general_ledger import check_freezing_date, is_immutable_ledger_enabled
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
|
||||
@@ -47,14 +46,6 @@ class PeriodClosingVoucher(AccountsController):
|
||||
self.block_if_future_closing_voucher_exists()
|
||||
self.check_closing_account_type()
|
||||
self.check_closing_account_currency()
|
||||
self.validate_accounts_not_frozen()
|
||||
|
||||
def validate_accounts_not_frozen(self, for_cancellation=False):
|
||||
posting_date = self.period_end_date
|
||||
if for_cancellation and is_immutable_ledger_enabled():
|
||||
posting_date = getdate()
|
||||
|
||||
check_freezing_date(posting_date, self.company)
|
||||
|
||||
def validate_start_and_end_date(self):
|
||||
self.fy_start_date, self.fy_end_date = frappe.db.get_value(
|
||||
@@ -156,7 +147,6 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"Process Period Closing Voucher",
|
||||
)
|
||||
self.block_if_future_closing_voucher_exists()
|
||||
self.validate_accounts_not_frozen(for_cancellation=True)
|
||||
|
||||
if not frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"):
|
||||
self.cancel_process_pcv_docs()
|
||||
|
||||
@@ -2802,15 +2802,12 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled("_Test Company 1")
|
||||
frappe.local.enable_perpetual_inventory["_Test Company 1"] = 1
|
||||
old_inventory_account = frappe.db.get_value("Company", "_Test Company 1", "default_inventory_account")
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
"_Test Company 1",
|
||||
{
|
||||
"stock_received_but_not_billed": "Stock Received But Not Billed - _TC1",
|
||||
"default_inventory_account": "Stock In Hand - _TC1",
|
||||
},
|
||||
"stock_received_but_not_billed",
|
||||
"Stock Received But Not Billed - _TC1",
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
@@ -2855,7 +2852,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
# tear down
|
||||
frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory
|
||||
frappe.db.set_value("Company", "_Test Company 1", "default_inventory_account", old_inventory_account)
|
||||
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock)
|
||||
|
||||
def test_sle_for_target_warehouse(self):
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
"options": "Cost Center",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "shipping_amount_section",
|
||||
@@ -138,20 +139,18 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-22 14:53:27.315435",
|
||||
"modified": "2019-05-25 23:12:26.156405",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Shipping Rule",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -197,8 +196,5 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
"sort_order": "ASC"
|
||||
}
|
||||
@@ -36,17 +36,18 @@ class ShippingRule(Document):
|
||||
from erpnext.accounts.doctype.shipping_rule_condition.shipping_rule_condition import (
|
||||
ShippingRuleCondition,
|
||||
)
|
||||
from erpnext.accounts.doctype.shipping_rule_country.shipping_rule_country import ShippingRuleCountry
|
||||
from erpnext.accounts.doctype.shipping_rule_country.shipping_rule_country import (
|
||||
ShippingRuleCountry,
|
||||
)
|
||||
|
||||
account: DF.Link
|
||||
calculate_based_on: DF.Literal["Fixed", "Net Total", "Net Weight"]
|
||||
company: DF.Link
|
||||
conditions: DF.Table[ShippingRuleCondition]
|
||||
cost_center: DF.Link | None
|
||||
cost_center: DF.Link
|
||||
countries: DF.Table[ShippingRuleCountry]
|
||||
disabled: DF.Check
|
||||
label: DF.Data
|
||||
project: DF.Link | None
|
||||
shipping_amount: DF.Currency
|
||||
shipping_rule_type: DF.Literal["Selling", "Buying"]
|
||||
# end: auto-generated types
|
||||
@@ -161,14 +162,7 @@ class ShippingRule(Document):
|
||||
)
|
||||
shipping_charge["add_deduct_tax"] = "Add"
|
||||
|
||||
shipping_charge_filters = shipping_charge.copy()
|
||||
if not self.cost_center:
|
||||
shipping_charge_filters["cost_center"] = (
|
||||
"in",
|
||||
(None, "", erpnext.get_default_cost_center(doc.company)),
|
||||
)
|
||||
|
||||
existing_shipping_charge = doc.get("taxes", filters=shipping_charge_filters)
|
||||
existing_shipping_charge = doc.get("taxes", filters=shipping_charge)
|
||||
if existing_shipping_charge:
|
||||
# take the last record found
|
||||
existing_shipping_charge[-1].tax_amount = shipping_amount
|
||||
|
||||
@@ -96,29 +96,3 @@ frappe.ui.form.on("Subscription", {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Subscription Plan Detail", {
|
||||
plan: function (frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
if (!row.plan) return;
|
||||
const requested_plan = row.plan;
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.subscription.subscription.get_plan_dimensions",
|
||||
args: {
|
||||
plan: requested_plan,
|
||||
company: frm.doc.company,
|
||||
party_type: frm.doc.party_type,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.message || locals[cdt]?.[cdn]?.plan !== requested_plan) return;
|
||||
// Only fill dimensions left empty, so a manual entry or an earlier plan is never overwritten.
|
||||
for (const [dimension, value] of Object.entries(r.message)) {
|
||||
if (frm.fields_dict[dimension] && !frm.doc[dimension]) {
|
||||
frm.set_value(dimension, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -26,7 +26,6 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
|
||||
|
||||
class InvoiceCancelled(frappe.ValidationError):
|
||||
@@ -748,39 +747,6 @@ def get_prorata_factor(
|
||||
return diff / plan_days
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_plan_dimensions(
|
||||
plan: str, company: str | None = None, party_type: str | None = None
|
||||
) -> dict[str, str]:
|
||||
"""Resolve a plan's accounting dimensions, falling back to the plan item's company defaults."""
|
||||
plan_doc = frappe.get_cached_doc("Subscription Plan", plan)
|
||||
|
||||
dimensions = {}
|
||||
for dimension in ["cost_center", *get_accounting_dimensions()]:
|
||||
value = plan_doc.get(dimension) or get_item_dimension(plan_doc.item, dimension, company, party_type)
|
||||
if value:
|
||||
dimensions[dimension] = value
|
||||
|
||||
return dimensions
|
||||
|
||||
|
||||
def get_item_dimension(
|
||||
item_code: str, dimension: str, company: str | None, party_type: str | None
|
||||
) -> str | None:
|
||||
if not company:
|
||||
return None
|
||||
|
||||
item_defaults = get_item_defaults(item_code, company)
|
||||
if dimension != "cost_center":
|
||||
return item_defaults.get(dimension)
|
||||
|
||||
selling = item_defaults.get("selling_cost_center")
|
||||
buying = item_defaults.get("buying_cost_center")
|
||||
if party_type == "Supplier":
|
||||
return buying or selling
|
||||
return selling or buying
|
||||
|
||||
|
||||
def process_all(subscription: list, posting_date: DateTimeLikeObject | None = None) -> None:
|
||||
"""
|
||||
Task to updates the status of all `Subscription` apart from those that are cancelled
|
||||
|
||||
@@ -17,7 +17,7 @@ from frappe.utils.data import (
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.accounts.doctype.subscription.subscription import get_plan_dimensions, get_prorata_factor
|
||||
from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor
|
||||
|
||||
test_dependencies = ("UOM", "Item Group", "Item")
|
||||
|
||||
@@ -583,48 +583,6 @@ class TestSubscription(FrappeTestCase):
|
||||
subscription.process(nowdate())
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
def test_plan_dimensions_resolve_from_plan_then_item(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
# Plan-level cost center takes precedence.
|
||||
create_plan(plan_name="_Test Sub Plan CC", cost=100, currency="INR")
|
||||
frappe.db.set_value(
|
||||
"Subscription Plan", "_Test Sub Plan CC", "cost_center", "_Test Cost Center - _TC"
|
||||
)
|
||||
self.assertEqual(
|
||||
get_plan_dimensions("_Test Sub Plan CC", "_Test Company", "Customer").get("cost_center"),
|
||||
"_Test Cost Center - _TC",
|
||||
)
|
||||
|
||||
# No plan cost center: fall back to the item's company default (selling vs buying by party type).
|
||||
item = make_item(
|
||||
"_Test Sub Dimension Item",
|
||||
{
|
||||
"is_stock_item": 0,
|
||||
"item_defaults": [
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"default_warehouse": "_Test Warehouse - _TC",
|
||||
"selling_cost_center": "_Test Cost Center - _TC",
|
||||
"buying_cost_center": "_Test Cost Center 2 - _TC",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
create_plan(plan_name="_Test Sub Plan No CC", cost=100, currency="INR", item=item.name)
|
||||
|
||||
self.assertEqual(
|
||||
get_plan_dimensions("_Test Sub Plan No CC", "_Test Company", "Customer").get("cost_center"),
|
||||
"_Test Cost Center - _TC",
|
||||
)
|
||||
self.assertEqual(
|
||||
get_plan_dimensions("_Test Sub Plan No CC", "_Test Company", "Supplier").get("cost_center"),
|
||||
"_Test Cost Center 2 - _TC",
|
||||
)
|
||||
|
||||
# Without a company the item fallback is skipped.
|
||||
self.assertNotIn("cost_center", get_plan_dimensions("_Test Sub Plan No CC"))
|
||||
|
||||
|
||||
def make_plans():
|
||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||
|
||||
@@ -849,12 +849,10 @@ def validate_account_party_type(self):
|
||||
|
||||
|
||||
def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
doctype = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
|
||||
if not frappe.has_permission(doctype, "read"):
|
||||
return None
|
||||
|
||||
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)
|
||||
|
||||
doctype = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
companies = frappe.get_list(
|
||||
doctype, filters={"docstatus": 1, party_type.lower(): party}, distinct=1, fields=["company"]
|
||||
)
|
||||
|
||||
@@ -106,7 +106,6 @@ class ReceivablePayableReport:
|
||||
|
||||
def get_data(self):
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
self.get_invoices_based_on_sales_partner()
|
||||
|
||||
# Get invoice details like bill_no, due_date etc for all invoices
|
||||
self.get_invoice_details()
|
||||
@@ -242,12 +241,6 @@ class ReceivablePayableReport:
|
||||
):
|
||||
return
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
# a return is folded onto the invoice it settles, so match that invoice's
|
||||
# partner (like the sales_person filter above), not the return's own
|
||||
if ple.against_voucher_no not in self.sales_partner_invoices:
|
||||
return
|
||||
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
else:
|
||||
@@ -476,7 +469,7 @@ class ReceivablePayableReport:
|
||||
"company": self.filters.company,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["name", "due_date", "po_no", "sales_partner"],
|
||||
fields=["name", "due_date", "po_no"],
|
||||
)
|
||||
for d in si_list:
|
||||
self.invoice_details.setdefault(d.name, d)
|
||||
@@ -910,22 +903,6 @@ class ReceivablePayableReport:
|
||||
for d in records:
|
||||
self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
|
||||
|
||||
def get_invoices_based_on_sales_partner(self):
|
||||
if not self.filters.get("sales_partner"):
|
||||
return
|
||||
|
||||
self.sales_partner_invoices = set(
|
||||
frappe.get_all(
|
||||
"Sales Invoice",
|
||||
filters={
|
||||
"sales_partner": self.filters.get("sales_partner"),
|
||||
"docstatus": 1,
|
||||
"company": self.filters.company,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
)
|
||||
|
||||
def prepare_conditions(self):
|
||||
self.qb_selection_filter = []
|
||||
self.or_filters = []
|
||||
@@ -1028,6 +1005,15 @@ class ReceivablePayableReport:
|
||||
|
||||
self.qb_selection_filter.append(Criterion.any([customer_ptt, sales_ptt]))
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
.where(self.customer.default_sales_partner == self.filters.get("sales_partner"))
|
||||
)
|
||||
)
|
||||
|
||||
def exclude_employee_transaction(self):
|
||||
self.qb_selection_filter.append(self.ple.party_type != "Employee")
|
||||
|
||||
@@ -1127,6 +1113,9 @@ class ReceivablePayableReport:
|
||||
if self.account_type == "Receivable":
|
||||
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
fields.append("default_sales_partner")
|
||||
|
||||
self.party_details[party] = frappe.db.get_value(
|
||||
"Customer",
|
||||
party,
|
||||
@@ -1253,7 +1242,7 @@ class ReceivablePayableReport:
|
||||
self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data")
|
||||
|
||||
if self.filters.sales_partner:
|
||||
self.add_column(label=_("Sales Partner"), fieldname="sales_partner", fieldtype="Data")
|
||||
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
|
||||
|
||||
if self.filters.account_type == "Payable":
|
||||
self.add_column(
|
||||
|
||||
@@ -7,7 +7,6 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
@@ -1304,61 +1303,3 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertIn(original_customer, parties)
|
||||
self.assertNotIn(second_customer, parties)
|
||||
self.assertEqual(allowed_invoice.customer, original_customer)
|
||||
|
||||
def test_receivable_filtered_by_sales_partner(self):
|
||||
frappe.set_user("Administrator")
|
||||
partner_a, partner_b = "_Test AR Sales Partner A", "_Test AR Sales Partner B"
|
||||
for partner in (partner_a, partner_b):
|
||||
if not frappe.db.exists("Sales Partner", partner):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Sales Partner",
|
||||
"partner_name": partner,
|
||||
"commission_rate": 0,
|
||||
"territory": "All Territories",
|
||||
}
|
||||
).insert()
|
||||
|
||||
def _si(sales_partner):
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True, qty=2)
|
||||
si.sales_partner = sales_partner
|
||||
return si.save().submit()
|
||||
|
||||
partner_a_si = _si(partner_a)
|
||||
partner_b_si = _si(partner_b)
|
||||
no_partner_si = _si(None)
|
||||
|
||||
# a return is folded onto the invoice it settles, so it nets against that
|
||||
# invoice's partner even when the return's own partner is cleared
|
||||
no_partner_return = make_return_doc("Sales Invoice", partner_a_si.name)
|
||||
no_partner_return.sales_partner = None
|
||||
no_partner_return.items[0].qty = -1
|
||||
no_partner_return.update_outstanding_for_self = 0
|
||||
no_partner_return.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
def rows_for(partner):
|
||||
return {
|
||||
r.voucher_no: r
|
||||
for r in execute({**filters, "sales_partner": partner})[1]
|
||||
if r.get("voucher_no")
|
||||
}
|
||||
|
||||
rows_a = rows_for(partner_a)
|
||||
self.assertIn(partner_a_si.name, rows_a)
|
||||
self.assertEqual(rows_a[partner_a_si.name].sales_partner, partner_a)
|
||||
self.assertNotIn(partner_b_si.name, rows_a)
|
||||
self.assertNotIn(no_partner_si.name, rows_a)
|
||||
self.assertNotIn(no_partner_return.name, rows_a)
|
||||
self.assertEqual(rows_a[partner_a_si.name].credit_note, 100)
|
||||
self.assertEqual(rows_a[partner_a_si.name].outstanding, 100)
|
||||
|
||||
rows_b = rows_for(partner_b)
|
||||
self.assertIn(partner_b_si.name, rows_b)
|
||||
self.assertNotIn(partner_a_si.name, rows_b)
|
||||
|
||||
@@ -132,8 +132,8 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
if row.sales_person:
|
||||
self.party_total[row.party].sales_person.append(row.get("sales_person", ""))
|
||||
|
||||
if self.filters.sales_partner and row.get("sales_partner"):
|
||||
self.party_total[row.party]["sales_partner"] = row.get("sales_partner")
|
||||
if self.filters.sales_partner:
|
||||
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner", "")
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
@@ -191,7 +191,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data")
|
||||
|
||||
if self.filters.sales_partner:
|
||||
self.add_column(label=_("Sales Partner"), fieldname="sales_partner", fieldtype="Data")
|
||||
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
|
||||
|
||||
else:
|
||||
self.add_column(
|
||||
|
||||
@@ -193,42 +193,3 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
report = execute(filters)
|
||||
rpt_output = report[1]
|
||||
self.assertEqual(len(rpt_output), 0)
|
||||
|
||||
def test_03_summary_sales_partner_column(self):
|
||||
partner = "_Test AR Summary Sales Partner"
|
||||
if not frappe.db.exists("Sales Partner", partner):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Sales Partner",
|
||||
"partner_name": partner,
|
||||
"commission_rate": 0,
|
||||
"territory": "All Territories",
|
||||
}
|
||||
).insert()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=200,
|
||||
price_list_rate=200,
|
||||
do_not_submit=True,
|
||||
)
|
||||
si.sales_partner = partner
|
||||
si.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"customer": self.customer,
|
||||
"posting_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"sales_partner": partner,
|
||||
}
|
||||
|
||||
rpt_output = execute(filters)[1]
|
||||
self.assertEqual(len(rpt_output), 1)
|
||||
self.assertEqual(rpt_output[0].get("sales_partner"), partner)
|
||||
|
||||
@@ -1239,7 +1239,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
return {
|
||||
"company": purchase_doc.company,
|
||||
"purchase_date": purchase_doc.get("posting_date"),
|
||||
"gross_purchase_amount": flt(first_item.valuation_rate) * flt(first_item.qty),
|
||||
"gross_purchase_amount": flt(first_item.base_net_amount),
|
||||
"asset_quantity": first_item.qty,
|
||||
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
|
||||
"asset_location": first_item.get("asset_location"),
|
||||
|
||||
@@ -734,7 +734,6 @@ def get_target_asset_details(asset=None, company=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_consumed_stock_item_details(args):
|
||||
frappe.has_permission("Stock Ledger Entry", throw=True)
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
@@ -744,7 +743,6 @@ def get_consumed_stock_item_details(args):
|
||||
item = frappe._dict()
|
||||
if args.item_code:
|
||||
item = frappe.get_cached_doc("Item", args.item_code)
|
||||
item.check_permission()
|
||||
|
||||
out.item_name = item.item_name
|
||||
out.batch_no = None
|
||||
@@ -754,8 +752,6 @@ def get_consumed_stock_item_details(args):
|
||||
out.stock_uom = item.stock_uom
|
||||
|
||||
out.warehouse = get_item_warehouse(item, args, overwrite_warehouse=True) if item else None
|
||||
if out.warehouse:
|
||||
frappe.has_permission("Warehouse", doc=out.warehouse, throw=True)
|
||||
|
||||
# Cost Center
|
||||
item_defaults = get_item_defaults(item.name, args.company)
|
||||
@@ -796,9 +792,6 @@ def get_warehouse_details(args):
|
||||
|
||||
out = {}
|
||||
if args.warehouse and args.item_code:
|
||||
frappe.has_permission("Item", doc=args.item_code, throw=True)
|
||||
frappe.has_permission("Warehouse", doc=args.warehouse, throw=True)
|
||||
frappe.has_permission("Stock Ledger Entry", throw=True)
|
||||
out = {
|
||||
"actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0,
|
||||
"valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False),
|
||||
|
||||
@@ -358,7 +358,7 @@ class BuyingController(SubcontractingController):
|
||||
)
|
||||
valuation_amount_adjustment -= item.item_tax_amount
|
||||
|
||||
self.round_floats_in(item, do_not_round_fields=["conversion_factor"])
|
||||
self.round_floats_in(item)
|
||||
if flt(item.conversion_factor) == 0.0:
|
||||
item.conversion_factor = (
|
||||
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
|
||||
|
||||
@@ -336,7 +336,6 @@ def create_variant(item, args, use_template_image=False):
|
||||
|
||||
@frappe.whitelist()
|
||||
def enqueue_multiple_variant_creation(item, args, use_template_image=False):
|
||||
frappe.has_permission("Item", ptype="create", throw=True)
|
||||
use_template_image = frappe.parse_json(use_template_image)
|
||||
# There can be innumerable attribute combinations, enqueue
|
||||
if isinstance(args, str):
|
||||
|
||||
@@ -306,9 +306,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_project_name(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | None = None
|
||||
):
|
||||
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
proj = qb.DocType("Project")
|
||||
qb_filter_and_conditions = []
|
||||
qb_filter_or_conditions = []
|
||||
@@ -323,7 +321,7 @@ def get_project_name(
|
||||
if filters.get("company"):
|
||||
qb_filter_and_conditions.append(proj.company == filters.get("company"))
|
||||
|
||||
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled", "On hold"]))
|
||||
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"]))
|
||||
|
||||
q = qb.from_(proj)
|
||||
|
||||
|
||||
@@ -158,28 +158,10 @@ def validate_returned_items(doc):
|
||||
):
|
||||
frappe.throw(_("Warehouse is mandatory"))
|
||||
|
||||
if doc.doctype in (
|
||||
"Purchase Invoice",
|
||||
"Purchase Receipt",
|
||||
"Subcontracting Receipt",
|
||||
"Sales Invoice",
|
||||
"Delivery Note",
|
||||
"POS Invoice",
|
||||
):
|
||||
if flt(d.qty) < 0 or flt(d.get("received_qty")) < 0:
|
||||
items_returned = True
|
||||
else:
|
||||
items_returned = True
|
||||
items_returned = True
|
||||
|
||||
elif d.item_name:
|
||||
if doc.doctype in ("Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"):
|
||||
# No item_code here means no linked Item, so there's no accepted/rejected
|
||||
# split to speak of - received_qty isn't a meaningful independent signal.
|
||||
# Only a negative qty (i.e. a real negative billing amount) counts.
|
||||
if flt(d.qty) < 0:
|
||||
items_returned = True
|
||||
else:
|
||||
items_returned = True
|
||||
items_returned = True
|
||||
|
||||
if not items_returned:
|
||||
frappe.throw(_("Atleast one item should be entered with negative quantity in return document"))
|
||||
|
||||
@@ -177,18 +177,13 @@ class StockController(AccountsController):
|
||||
)
|
||||
|
||||
is_asset_pr = any(d.get("is_fixed_asset") for d in self.get("items"))
|
||||
need_inventory_map = (self.get_stock_items() or self.get("packed_items")) and cint(
|
||||
erpnext.is_perpetual_inventory_enabled(self.company)
|
||||
)
|
||||
|
||||
if (
|
||||
cint(erpnext.is_perpetual_inventory_enabled(self.company))
|
||||
or provisional_accounting_for_non_stock_items
|
||||
or is_asset_pr
|
||||
):
|
||||
warehouse_account = frappe._dict()
|
||||
if need_inventory_map:
|
||||
warehouse_account = get_warehouse_account_map(self.company)
|
||||
warehouse_account = get_warehouse_account_map(self.company)
|
||||
|
||||
if self.docstatus == 1:
|
||||
if not gl_entries:
|
||||
|
||||
@@ -185,12 +185,7 @@ class calculate_taxes_and_totals:
|
||||
return
|
||||
|
||||
if not self.discount_amount_applied:
|
||||
do_not_round_fields = [
|
||||
"valuation_rate",
|
||||
"incoming_rate",
|
||||
"sales_incoming_rate",
|
||||
"conversion_factor",
|
||||
]
|
||||
do_not_round_fields = ["valuation_rate", "incoming_rate", "sales_incoming_rate"]
|
||||
|
||||
for item in self.doc.items:
|
||||
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestSalesAndPurchaseReturn(FrappeTestCase):
|
||||
@staticmethod
|
||||
def _cancel_and_delete(doctype, name):
|
||||
if not frappe.db.exists(doctype, name):
|
||||
return
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
if doc.docstatus == 1:
|
||||
doc.cancel()
|
||||
frappe.delete_doc(doctype, name, force=1)
|
||||
|
||||
def test_purchase_invoice_zero_qty_return_is_rejected(self):
|
||||
# A return with every item at qty 0 moves no stock and no value, so it must be
|
||||
# rejected the same way a return with no items at all would be.
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
|
||||
pi = make_purchase_invoice(qty=10)
|
||||
self.addCleanup(self._cancel_and_delete, "Purchase Invoice", pi.name)
|
||||
|
||||
return_pi = make_purchase_invoice(
|
||||
is_return=1,
|
||||
return_against=pi.name,
|
||||
qty=0,
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, return_pi.save)
|
||||
|
||||
def test_purchase_invoice_item_name_only_zero_qty_return_is_rejected(self):
|
||||
# Item Code is not mandatory on Purchase Invoice Item - a row can have only an
|
||||
# item_name (e.g. a free-text/non-stock line). Such rows fall through to the
|
||||
# item_name-only branch, which must also reject an all-zero-qty return instead
|
||||
# of unconditionally treating the row as returned.
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
|
||||
pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
|
||||
pi.items[0].item_code = ""
|
||||
pi.save()
|
||||
pi.submit()
|
||||
self.addCleanup(self._cancel_and_delete, "Purchase Invoice", pi.name)
|
||||
|
||||
return_pi = make_purchase_invoice(
|
||||
item_name="_Test Item",
|
||||
is_return=1,
|
||||
return_against=pi.name,
|
||||
qty=0,
|
||||
do_not_save=True,
|
||||
)
|
||||
return_pi.items[0].item_code = ""
|
||||
|
||||
self.assertRaises(frappe.ValidationError, return_pi.save)
|
||||
|
||||
def test_delivery_note_zero_qty_return_is_rejected(self):
|
||||
# A return with every item at qty 0 moves no stock and no value, so it must be
|
||||
# rejected the same way a return with no items at all would be.
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=100)
|
||||
self.addCleanup(self._cancel_and_delete, "Stock Entry", se.name)
|
||||
|
||||
dn = create_delivery_note(qty=5)
|
||||
self.addCleanup(self._cancel_and_delete, "Delivery Note", dn.name)
|
||||
|
||||
return_dn = make_sales_return(dn.name)
|
||||
return_dn.items[0].qty = 0
|
||||
|
||||
self.assertRaises(frappe.ValidationError, return_dn.insert)
|
||||
|
||||
def test_sales_invoice_zero_qty_return_is_rejected(self):
|
||||
# Same rule for a standalone (non stock-affecting) Sales Invoice return: qty 0 on
|
||||
# every row must be rejected, not silently accepted as a no-op credit note.
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
si = create_sales_invoice(qty=10)
|
||||
self.addCleanup(self._cancel_and_delete, "Sales Invoice", si.name)
|
||||
|
||||
return_si = make_return_doc(si.doctype, si.name)
|
||||
return_si.items[0].qty = 0
|
||||
|
||||
self.assertRaises(frappe.ValidationError, return_si.save)
|
||||
@@ -132,7 +132,6 @@ class Opportunity(TransactionBase, CRMNote):
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_cust_name()
|
||||
self.map_fields()
|
||||
self.validate_qty()
|
||||
self.set_exchange_rate()
|
||||
|
||||
if not self.title:
|
||||
@@ -143,15 +142,6 @@ class Opportunity(TransactionBase, CRMNote):
|
||||
def on_update(self):
|
||||
self.update_prospect()
|
||||
|
||||
def validate_qty(self):
|
||||
for item in self.items:
|
||||
if flt(item.qty) <= 0:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Quantity must be greater than 0 for Item {1}").format(
|
||||
item.idx, item.item_code
|
||||
)
|
||||
)
|
||||
|
||||
def map_fields(self):
|
||||
for field in self.meta.get_valid_columns():
|
||||
if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field):
|
||||
|
||||
@@ -67,14 +67,6 @@ class PlantFloor(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_summary(warehouse, start=0, item_code=None, item_group=None):
|
||||
frappe.has_permission("Warehouse", doc=warehouse, throw=True)
|
||||
|
||||
if item_code:
|
||||
frappe.has_permission("Item", doc=item_code, throw=True)
|
||||
|
||||
if item_group:
|
||||
frappe.has_permission("Item Group", doc=item_group, throw=True)
|
||||
|
||||
stock_details = get_stock_details(warehouse, start=start, item_code=item_code, item_group=item_group)
|
||||
|
||||
max_count = 0.0
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<div class="row" style="border-bottom:1px solid var(--border-color); padding:4px 5px; margin-top: 3px;margin-bottom: 3px;">
|
||||
<div class="col-sm-1">
|
||||
{% if(row.image) { %}
|
||||
<img style="width:50px;height:50px;" src="{{frappe.utils.escape_html(row.image)}}">
|
||||
<img style="width:50px;height:50px;" src="{{row.image}}">
|
||||
{% } else { %}
|
||||
<div style="width:50px;height:50px;background-color:var(--control-bg);text-align:center;padding-top:15px">{{frappe.get_abbr(frappe.utils.escape_html(row.item_code), 2)}}</div>
|
||||
<div style="width:50px;height:50px;background-color:var(--control-bg);text-align:center;padding-top:15px">{{frappe.get_abbr(row.item_code, 2)}}</div>
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
@@ -13,7 +13,7 @@
|
||||
{% } else { %}
|
||||
{{row.item_link}}
|
||||
<p>
|
||||
{{frappe.utils.escape_html(row.item_name)}}
|
||||
{{row.item_name}}
|
||||
</p>
|
||||
{% } %}
|
||||
|
||||
@@ -52,10 +52,10 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add" data-item-code="{{ frappe.utils.escape_html(row.item_code) }}">{{ __("Add") }}</button>
|
||||
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add" data-item-code="{{ escape(row.item_code) }}">{{ __("Add") }}</button>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-move" data-item-code="{{ frappe.utils.escape_html(row.item_code) }}">{{ __("Move") }}</button>
|
||||
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-move" data-item-code="{{ escape(row.item_code) }}">{{ __("Move") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% }); %}
|
||||
|
||||
@@ -46,60 +46,3 @@ frappe.views.calendar["Work Order"] = {
|
||||
],
|
||||
get_events_method: "frappe.desk.calendar.get_events",
|
||||
};
|
||||
|
||||
const WORK_ORDER_GANTT_COLORS = {
|
||||
Draft: "red",
|
||||
Stopped: "red",
|
||||
"Not Started": "red",
|
||||
"In Process": "orange",
|
||||
Completed: "green",
|
||||
"Stock Reserved": "blue",
|
||||
"Stock Partially Reserved": "orange",
|
||||
Cancelled: "gray",
|
||||
};
|
||||
|
||||
if (!frappe.views.GanttView.prototype._work_order_status_colors) {
|
||||
frappe.views.GanttView.prototype._work_order_status_colors = true;
|
||||
|
||||
const prepare_tasks = frappe.views.GanttView.prototype.prepare_tasks;
|
||||
frappe.views.GanttView.prototype.prepare_tasks = function () {
|
||||
prepare_tasks.call(this);
|
||||
if (this.doctype === "Work Order") {
|
||||
set_work_order_bar_classes(this);
|
||||
}
|
||||
};
|
||||
|
||||
const set_colors = frappe.views.GanttView.prototype.set_colors;
|
||||
frappe.views.GanttView.prototype.set_colors = function () {
|
||||
set_colors.call(this);
|
||||
if (this.doctype === "Work Order") {
|
||||
set_work_order_bar_styles(this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function set_work_order_bar_classes(view) {
|
||||
view.tasks.forEach((task, idx) => {
|
||||
const color = WORK_ORDER_GANTT_COLORS[view.data[idx].status];
|
||||
if (color) {
|
||||
task.custom_class = "wo-" + color;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function set_work_order_bar_styles(view) {
|
||||
const style = [...new Set(Object.values(WORK_ORDER_GANTT_COLORS))]
|
||||
.map(
|
||||
(color) => `
|
||||
.gantt .bar-wrapper.wo-${color} .bar {
|
||||
fill: var(--${color}-300);
|
||||
}
|
||||
.gantt .bar-wrapper.wo-${color} .bar-progress {
|
||||
fill: var(--${color}-300);
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
view.$result.prepend(`<style>${style}</style>`);
|
||||
}
|
||||
|
||||
@@ -418,6 +418,6 @@ def get_workstations(**kwargs):
|
||||
d.background_color = color_map.get(d.status, "var(--red-600)")
|
||||
d.workstation_link = get_url_to_form("Workstation", d.name)
|
||||
if d.status != "Production":
|
||||
d.status_image = frappe.utils.escape_html(d.off_status_image)
|
||||
d.status_image = d.off_status_image
|
||||
|
||||
return data
|
||||
|
||||
@@ -446,4 +446,3 @@ erpnext.patches.v16_0.access_control_for_project_users
|
||||
erpnext.patches.v16_0.rename_ar_ap_ageing_filter
|
||||
erpnext.patches.v15_0.fix_titles
|
||||
erpnext.patches.v16_0.backfill_repost_accounting_ledger_status
|
||||
erpnext.patches.v16_0.merge_seeded_item_group_root
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import frappe
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
SEEDED_ROOT = "All Item Groups"
|
||||
|
||||
|
||||
def execute():
|
||||
"""Collapse the "All Item Groups" node seeded under a pre-existing root.
|
||||
|
||||
Setup seeding always inserted "All Item Groups" as a parentless group. On a
|
||||
site where another app had already created the root (under a translated
|
||||
name), it was re-parented instead, leaving a second group-root holding the
|
||||
standard Item Groups.
|
||||
"""
|
||||
root = get_root_of("Item Group")
|
||||
if not root or root == SEEDED_ROOT:
|
||||
return
|
||||
|
||||
seeded = frappe.db.get_value("Item Group", SEEDED_ROOT, ["parent_item_group", "is_group"], as_dict=True)
|
||||
if not seeded or not seeded.is_group or seeded.parent_item_group != root:
|
||||
return
|
||||
|
||||
frappe.rename_doc("Item Group", SEEDED_ROOT, root, merge=True, show_alert=False)
|
||||
@@ -282,23 +282,6 @@ class TestProject(FrappeTestCase):
|
||||
project.save()
|
||||
self.assertEqual(project.percent_complete, 100)
|
||||
|
||||
def test_on_hold_project_keeps_status(self):
|
||||
project, tasks = self._project_with_tasks("Task Completion", 4)
|
||||
|
||||
# an On hold project is not auto-flipped to Completed even at 100%
|
||||
project.status = "On hold"
|
||||
for task in tasks:
|
||||
frappe.db.set_value("Task", task, "status", "Completed")
|
||||
project.update_percent_complete()
|
||||
self.assertEqual(project.percent_complete, 100)
|
||||
self.assertEqual(project.status, "On hold")
|
||||
|
||||
# nor auto-flipped back to Open when below 100%
|
||||
frappe.db.set_value("Task", tasks[0], "status", "Open")
|
||||
project.update_percent_complete()
|
||||
self.assertEqual(project.percent_complete, 75)
|
||||
self.assertEqual(project.status, "On hold")
|
||||
|
||||
def _create_portal_user(self, email):
|
||||
"""A user with no Project-related role, so read access can only come from
|
||||
control_access_for_project_users() sharing the doc with them."""
|
||||
|
||||
@@ -15,12 +15,6 @@ frappe.ui.form.on("Task", {
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
frm.set_query("project", function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("task", "depends_on", function () {
|
||||
let filters = {
|
||||
name: ["!=", frm.doc.name],
|
||||
|
||||
@@ -30,7 +30,6 @@ frappe.ui.form.on("Timesheet", {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
status: "Open",
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -123,7 +122,6 @@ frappe.ui.form.on("Timesheet", {
|
||||
return {
|
||||
filters: {
|
||||
customer: doc.customer,
|
||||
status: "Open",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ frappe.query_reports["Project Summary"] = {
|
||||
fieldname: "status",
|
||||
label: __("Status"),
|
||||
fieldtype: "Select",
|
||||
options: "\nOpen\nOn hold\nCompleted\nCancelled",
|
||||
options: "\nOpen\nCompleted\nCancelled",
|
||||
default: "Open",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -126,26 +126,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
}
|
||||
|
||||
get_item_fields_to_round() {
|
||||
const [item] = this.frm.doc.items || [];
|
||||
if (!item) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const do_not_round_fields = ["conversion_factor"];
|
||||
return frappe.meta
|
||||
.get_fieldnames(item.doctype, item.parent, {
|
||||
fieldtype: ["in", ["Currency", "Float"]],
|
||||
})
|
||||
.filter((fieldname) => !do_not_round_fields.includes(fieldname));
|
||||
}
|
||||
|
||||
calculate_item_values() {
|
||||
var me = this;
|
||||
if (!this.discount_amount_applied) {
|
||||
const fields_to_round = this.get_item_fields_to_round();
|
||||
for (const item of this.frm.doc.items || []) {
|
||||
frappe.model.round_floats_in(item, fields_to_round);
|
||||
frappe.model.round_floats_in(item);
|
||||
item.net_rate = item.rate;
|
||||
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
<div class="app-listing item-list image-view-container item-selector">
|
||||
{% for (var i=0; i < data.length; i++) { var item = data[i]; %}
|
||||
{% const item_name = frappe.utils.escape_html(item.name); %}
|
||||
{% const item_title = frappe.utils.escape_html(item.item_name || item.name); %}
|
||||
{% if (i % 4 === 0) { %}<div class="image-view-row">{% } %}
|
||||
<div class="image-view-item" data-name="{{ item_name }}">
|
||||
<div class="image-view-item" data-name="{{ item.name }}">
|
||||
<div class="image-view-header doclist-row">
|
||||
<div class="list-value">
|
||||
<a class="grey list-id" data-name="{{ item_name }}"
|
||||
title="{{ item_title }}">
|
||||
{{ item_title }}</a>
|
||||
<a class="grey list-id" data-name="{{item.name}}"
|
||||
title="{{ item.item_name || item.name}}">
|
||||
{{item.item_name || item.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-view-body">
|
||||
<a data-item-code="{{ item_name }}"
|
||||
title="{{ item_title }}"
|
||||
<a data-item-code="{{ item.name }}"
|
||||
title="{{ item.item_name || item.name }}"
|
||||
>
|
||||
<div class="image-field"
|
||||
style="
|
||||
@@ -24,11 +22,11 @@
|
||||
>
|
||||
{% if (!item.image) { %}
|
||||
<span class="placeholder-text">
|
||||
{%= frappe.get_abbr(item_title) %}
|
||||
{%= frappe.get_abbr(item.item_name || item.name) %}
|
||||
</span>
|
||||
{% } %}
|
||||
{% if (item.image) { %}
|
||||
<img src="{{ frappe.utils.escape_html(item.image) }}" alt="{{ item_title }}">
|
||||
<img src="{{ item.image }}" alt="{{item.item_name || item.name}}">
|
||||
{% } %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{% $.each(workstations, (idx, row) => { %}
|
||||
{% const row_workstation_name = frappe.utils.escape_html(row.name); %}
|
||||
<div class="workstation-wrapper">
|
||||
<div class="workstation-status text-right">
|
||||
{% if(row.status == "Production") { %}
|
||||
@@ -18,14 +17,14 @@
|
||||
{% if(row.status_image) { %}
|
||||
<img class="workstation-image-cls" src="{{row.status_image}}">
|
||||
{% } else { %}
|
||||
<div class="workstation-image-cls workstation-abbr">{{frappe.get_abbr(row_workstation_name, 2)}}</div>
|
||||
<div class="workstation-image-cls workstation-abbr">{{frappe.get_abbr(row.name, 2)}}</div>
|
||||
{% } %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workstation-card" style="display: grid;">
|
||||
<span class="ellipsis" title="{{row_workstation_name}}">
|
||||
{{row_workstation_name}}
|
||||
<span class="ellipsis" title="{{row.name}}">
|
||||
{{row.workstation_name}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -147,9 +147,6 @@ class Quotation(SellingController):
|
||||
|
||||
make_packing_list(self)
|
||||
|
||||
def after_insert(self):
|
||||
self.carry_forward_communication()
|
||||
|
||||
def before_submit(self):
|
||||
self.set_has_alternative_item()
|
||||
|
||||
@@ -295,6 +292,7 @@ class Quotation(SellingController):
|
||||
# update enquiry status
|
||||
self.update_opportunity("Quotation")
|
||||
self.update_lead()
|
||||
self.carry_forward_communication()
|
||||
|
||||
def on_cancel(self):
|
||||
if self.lost_reasons:
|
||||
|
||||
@@ -1984,41 +1984,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
sales_order.save()
|
||||
self.assertEqual(sales_order.taxes[0].tax_amount, 0)
|
||||
|
||||
def test_sales_order_with_shipping_rule_without_cost_center(self):
|
||||
from erpnext import get_default_cost_center
|
||||
|
||||
shipping_rule = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Shipping Rule",
|
||||
"label": "Shipping Rule Without Cost Center - Sales Order Test",
|
||||
"shipping_rule_type": "Selling",
|
||||
"company": "_Test Company",
|
||||
"account": "_Test Account Shipping Charges - _TC",
|
||||
"calculate_based_on": "Fixed",
|
||||
"shipping_amount": 50,
|
||||
}
|
||||
).insert()
|
||||
sales_order = make_sales_order(do_not_save=True)
|
||||
sales_order.shipping_rule = shipping_rule.name
|
||||
company_cost_center = get_default_cost_center(sales_order.company)
|
||||
|
||||
shipping_rule.apply(sales_order)
|
||||
self.assertEqual(len(sales_order.taxes), 1)
|
||||
self.assertIsNone(sales_order.taxes[0].cost_center)
|
||||
|
||||
for cost_center in (None, "", company_cost_center):
|
||||
sales_order.taxes[0].cost_center = cost_center
|
||||
shipping_rule.apply(sales_order)
|
||||
self.assertEqual(len(sales_order.taxes), 1)
|
||||
self.assertEqual(sales_order.taxes[0].cost_center, cost_center)
|
||||
|
||||
sales_order.taxes[0].cost_center = ""
|
||||
sales_order.save()
|
||||
sales_order.reload()
|
||||
shipping_rule.apply(sales_order)
|
||||
self.assertEqual(len(sales_order.taxes), 1)
|
||||
self.assertEqual(sales_order.taxes[0].cost_center, "")
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 1},
|
||||
|
||||
@@ -53,7 +53,7 @@ class Employee(NestedSet):
|
||||
user = frappe.get_doc("User", existing_user_id)
|
||||
validate_employee_role(user, ignore_emp_check=True)
|
||||
user.save(ignore_permissions=True)
|
||||
remove_user_permission("Employee", self.name, existing_user_id, ignore_permissions=True)
|
||||
remove_user_permission("Employee", self.name, existing_user_id)
|
||||
|
||||
def after_rename(self, old, new, merge):
|
||||
self.db_set("employee", new)
|
||||
@@ -91,11 +91,11 @@ class Employee(NestedSet):
|
||||
)
|
||||
|
||||
if employee_user_permission_exists and not self.create_user_permission:
|
||||
remove_user_permission("Employee", self.name, self.user_id, ignore_permissions=True)
|
||||
remove_user_permission("Company", self.company, self.user_id, ignore_permissions=True)
|
||||
remove_user_permission("Employee", self.name, self.user_id)
|
||||
remove_user_permission("Company", self.company, self.user_id)
|
||||
elif not employee_user_permission_exists and self.create_user_permission:
|
||||
add_user_permission("Employee", self.name, self.user_id, ignore_permissions=True)
|
||||
add_user_permission("Company", self.company, self.user_id, ignore_permissions=True)
|
||||
add_user_permission("Employee", self.name, self.user_id)
|
||||
add_user_permission("Company", self.company, self.user_id)
|
||||
|
||||
def update_user(self):
|
||||
# add employee role if missing
|
||||
|
||||
@@ -16,8 +16,6 @@ from frappe.utils.nestedset import (
|
||||
|
||||
test_records = frappe.get_test_records("Item Group")
|
||||
|
||||
TRANSLATED_ROOT = "Todos os Grupos de Itens"
|
||||
|
||||
|
||||
class TestItem(unittest.TestCase):
|
||||
def test_basic_tree(self, records=None):
|
||||
@@ -236,46 +234,3 @@ class TestItem(unittest.TestCase):
|
||||
"_Test Item Group B - 3",
|
||||
merge=True,
|
||||
)
|
||||
|
||||
def test_patch_merges_seeded_root_into_existing_root(self):
|
||||
from erpnext.patches.v16_0.merge_seeded_item_group_root import execute
|
||||
|
||||
self.nest_root_under(TRANSLATED_ROOT)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Item Group", "All Item Groups", "parent_item_group"), TRANSLATED_ROOT
|
||||
)
|
||||
|
||||
execute()
|
||||
|
||||
self.assertFalse(frappe.db.exists("Item Group", "All Item Groups"))
|
||||
self.assertEqual(self.get_root_names(), [TRANSLATED_ROOT])
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Item Group", "_Test Item Group B", "parent_item_group"), TRANSLATED_ROOT
|
||||
)
|
||||
self.test_basic_tree()
|
||||
|
||||
# restore the original root name for the tests that follow
|
||||
frappe.rename_doc("Item Group", TRANSLATED_ROOT, "All Item Groups")
|
||||
self.assertEqual(self.get_root_names(), ["All Item Groups"])
|
||||
self.test_basic_tree()
|
||||
|
||||
def nest_root_under(self, new_root):
|
||||
"""Recreate the tree left behind by seeding a root under a pre-existing one."""
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"item_group_name": new_root,
|
||||
"is_group": 1,
|
||||
"parent_item_group": "All Item Groups",
|
||||
}
|
||||
).insert()
|
||||
|
||||
ig = frappe.qb.DocType("Item Group")
|
||||
frappe.qb.update(ig).set(ig.parent_item_group, "").where(ig.name == new_root).run()
|
||||
frappe.qb.update(ig).set(ig.parent_item_group, new_root).where(ig.name == "All Item Groups").run()
|
||||
rebuild_tree("Item Group", "parent_item_group")
|
||||
|
||||
def get_root_names(self):
|
||||
return frappe.db.sql_list(
|
||||
"""select name from `tabItem Group` where ifnull(parent_item_group, '')=''"""
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ from frappe.desk.doctype.global_search_settings.global_search_settings import (
|
||||
)
|
||||
from frappe.desk.page.setup_wizard.setup_wizard import make_records
|
||||
from frappe.utils import cstr, getdate
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.accounts.doctype.account.account import RootNotEditable
|
||||
from erpnext.regional.address_template.setup import set_up_address_templates
|
||||
@@ -25,48 +24,46 @@ def read_lines(filename: str) -> list[str]:
|
||||
|
||||
|
||||
def install(country=None):
|
||||
root_item_group = get_root_of("Item Group") or _("All Item Groups")
|
||||
records = [
|
||||
# ensure at least an empty Address Template exists for this Country
|
||||
{"doctype": "Address Template", "country": country},
|
||||
# item group
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"item_group_name": root_item_group,
|
||||
"item_group_name": _("All Item Groups"),
|
||||
"is_group": 1,
|
||||
"parent_item_group": "",
|
||||
"__condition": lambda: not frappe.db.exists("Item Group", root_item_group),
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"item_group_name": _("Products"),
|
||||
"is_group": 0,
|
||||
"parent_item_group": root_item_group,
|
||||
"parent_item_group": _("All Item Groups"),
|
||||
"show_in_website": 1,
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"item_group_name": _("Raw Material"),
|
||||
"is_group": 0,
|
||||
"parent_item_group": root_item_group,
|
||||
"parent_item_group": _("All Item Groups"),
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"item_group_name": _("Services"),
|
||||
"is_group": 0,
|
||||
"parent_item_group": root_item_group,
|
||||
"parent_item_group": _("All Item Groups"),
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"item_group_name": _("Sub Assemblies"),
|
||||
"is_group": 0,
|
||||
"parent_item_group": root_item_group,
|
||||
"parent_item_group": _("All Item Groups"),
|
||||
},
|
||||
{
|
||||
"doctype": "Item Group",
|
||||
"item_group_name": _("Consumable"),
|
||||
"is_group": 0,
|
||||
"parent_item_group": root_item_group,
|
||||
"parent_item_group": _("All Item Groups"),
|
||||
},
|
||||
# Stock Entry Type
|
||||
{
|
||||
|
||||
@@ -79,13 +79,10 @@ def get_warehouse_account(warehouse, warehouse_account=None):
|
||||
account = get_company_default_inventory_account(warehouse.company)
|
||||
|
||||
if not account and warehouse.company:
|
||||
inventory_accounts = frappe.get_all(
|
||||
"Account", {"account_type": "Stock", "is_group": 0, "company": warehouse.company}, pluck="name"
|
||||
account = frappe.db.get_value(
|
||||
"Account", {"account_type": "Stock", "is_group": 0, "company": warehouse.company}, "name"
|
||||
)
|
||||
|
||||
if len(inventory_accounts) == 1:
|
||||
account = inventory_accounts[0]
|
||||
|
||||
if not account and warehouse.company and not warehouse.is_group:
|
||||
frappe.throw(
|
||||
_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}").format(
|
||||
|
||||
@@ -706,76 +706,6 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
self.assertEqual(gle_warehouse_amount, 1400)
|
||||
|
||||
def test_return_bundle_voucher_detail_no_as_packed_item(self):
|
||||
"""Return bundle whose voucher_detail_no is the Packed Item (SLE-driven path) must still value on repost."""
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
packed_item = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-DN-RET-VDN-.#####",
|
||||
}
|
||||
).name
|
||||
bundle_item = make_item(properties={"is_stock_item": 0, "is_sales_item": 1}).name
|
||||
make_product_bundle(bundle_item, [packed_item], qty=20)
|
||||
|
||||
make_stock_entry(item_code=packed_item, target=warehouse, qty=60, basic_rate=35)
|
||||
|
||||
dn = create_delivery_note(item_code=bundle_item, warehouse=warehouse, qty=3)
|
||||
|
||||
return_dn = make_sales_return(dn.name)
|
||||
return_dn.items[0].qty = -2
|
||||
return_dn.submit()
|
||||
return_dn.reload()
|
||||
|
||||
packed_row = return_dn.packed_items[0]
|
||||
bundle = frappe.get_doc("Serial and Batch Bundle", packed_row.serial_and_batch_bundle)
|
||||
|
||||
# Reproduce the reported state: bundle points at the Packed Item (not the DN Item), valuation at 0.
|
||||
bundle.db_set("voucher_detail_no", packed_row.name)
|
||||
bundle.db_set({"avg_rate": 0, "total_amount": 0})
|
||||
for entry in bundle.entries:
|
||||
entry.db_set({"incoming_rate": 0, "stock_value_difference": 0})
|
||||
packed_row.db_set("incoming_rate", 0)
|
||||
frappe.db.set_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Delivery Note",
|
||||
"voucher_no": return_dn.name,
|
||||
"item_code": packed_item,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
{"incoming_rate": 0, "stock_value_difference": 0},
|
||||
)
|
||||
|
||||
frappe.get_doc(
|
||||
doctype="Repost Item Valuation",
|
||||
based_on="Transaction",
|
||||
voucher_type="Delivery Note",
|
||||
voucher_no=return_dn.name,
|
||||
posting_date=return_dn.posting_date,
|
||||
posting_time=return_dn.posting_time,
|
||||
).submit()
|
||||
|
||||
bundle.reload()
|
||||
self.assertEqual(flt(bundle.avg_rate), 35)
|
||||
|
||||
incoming_rate, stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Delivery Note",
|
||||
"voucher_no": return_dn.name,
|
||||
"item_code": packed_item,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
["incoming_rate", "stock_value_difference"],
|
||||
)
|
||||
self.assertEqual(flt(incoming_rate), 35)
|
||||
self.assertEqual(flt(stock_value_difference), 1400)
|
||||
|
||||
def test_bin_details_of_packed_item(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
@@ -837,17 +837,7 @@ class Item(Document):
|
||||
frappe.throw(_("Item {0} is not a template item.").format(frappe.bold(self.variant_of)))
|
||||
|
||||
if based_on == "Item Attribute":
|
||||
previous_doc = self.get_doc_before_save()
|
||||
saved_attributes = (
|
||||
{(row.attribute, row.attribute_value) for row in previous_doc.attributes}
|
||||
if previous_doc
|
||||
else set()
|
||||
)
|
||||
|
||||
for d in self.attributes:
|
||||
if (d.attribute, d.attribute_value) in saved_attributes:
|
||||
continue
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Item Variant Attribute", {"attribute": d.attribute, "parent": self.variant_of}
|
||||
):
|
||||
|
||||
@@ -360,45 +360,6 @@ class TestItem(FrappeTestCase):
|
||||
self.assertRaises(InvalidItemAttributeValueError, attribute.save)
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_disabled_attribute_blocks_only_attribute_changes(self):
|
||||
frappe.delete_doc_if_exists("Item", "_Test Disabled Attribute Template-L", force=1)
|
||||
frappe.delete_doc_if_exists("Item", "_Test Disabled Attribute Template", force=1)
|
||||
frappe.delete_doc_if_exists("Item Attribute", "_Test Disabled Size", force=1)
|
||||
|
||||
attribute = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Attribute",
|
||||
"attribute_name": "_Test Disabled Size",
|
||||
"item_attribute_values": [
|
||||
{"attribute_value": "Large", "abbr": "L"},
|
||||
{"attribute_value": "Small", "abbr": "S"},
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
template = make_item(
|
||||
"_Test Disabled Attribute Template",
|
||||
{
|
||||
"has_variants": 1,
|
||||
"variant_based_on": "Item Attribute",
|
||||
"attributes": [{"attribute": attribute.name}],
|
||||
},
|
||||
)
|
||||
|
||||
variant = create_variant(template.name, {attribute.name: "Large"})
|
||||
variant.save()
|
||||
|
||||
attribute.disabled = 1
|
||||
attribute.save()
|
||||
|
||||
variant.reload()
|
||||
variant.description = "Edited after the attribute was disabled"
|
||||
variant.save()
|
||||
|
||||
variant.reload()
|
||||
variant.attributes[0].attribute_value = "Small"
|
||||
self.assertRaises(frappe.ValidationError, variant.save)
|
||||
|
||||
def test_rename_attribute_value_updates_variants(self):
|
||||
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)
|
||||
|
||||
|
||||
@@ -194,10 +194,8 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
|
||||
epi = is_perpetual_inventory_enabled(company_a)
|
||||
company_doc = frappe.get_doc("Company", company_a)
|
||||
old_inventory_account = company_doc.default_inventory_account
|
||||
company_doc.enable_perpetual_inventory = 1
|
||||
company_doc.stock_received_but_not_billed = srbnb
|
||||
company_doc.default_inventory_account = "Stock In Hand - _TC"
|
||||
company_doc.save()
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
@@ -225,11 +223,7 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
distribute_landed_cost_on_items(lcv)
|
||||
lcv.submit()
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company_a,
|
||||
{"enable_perpetual_inventory": epi, "default_inventory_account": old_inventory_account},
|
||||
)
|
||||
frappe.db.set_value("Company", company_a, "enable_perpetual_inventory", epi)
|
||||
frappe.local.enable_perpetual_inventory = {}
|
||||
|
||||
def test_landed_cost_voucher_for_zero_purchase_rate(self):
|
||||
|
||||
@@ -795,28 +795,6 @@ class TestMaterialRequest(FrappeTestCase):
|
||||
mr = frappe.get_doc("Material Request", mr.name)
|
||||
self.assertEqual(mr.per_ordered, 100)
|
||||
|
||||
def test_fractional_conversion_factor_for_purchase(self):
|
||||
item = create_item("_Test Fractional Conversion Item", stock_uom="Kg", is_purchase_item=1)
|
||||
conversion_factor = 0.453592292
|
||||
|
||||
mr = make_material_request(
|
||||
item_code=item.name,
|
||||
qty=1000,
|
||||
uom="Pound",
|
||||
conversion_factor=conversion_factor,
|
||||
)
|
||||
mr.reload()
|
||||
|
||||
self.assertEqual(mr.items[0].conversion_factor, conversion_factor)
|
||||
|
||||
po = make_purchase_order(mr.name)
|
||||
po.supplier = "_Test Supplier"
|
||||
po.insert()
|
||||
po.reload()
|
||||
|
||||
self.assertEqual(po.items[0].conversion_factor, conversion_factor)
|
||||
self.assertEqual(po.items[0].stock_qty, mr.items[0].stock_qty)
|
||||
|
||||
def test_customer_provided_parts_mr(self):
|
||||
create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)
|
||||
existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")
|
||||
|
||||
@@ -404,10 +404,29 @@ class PurchaseReceipt(BuyingController):
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
self.reserve_stock_for_sales_order()
|
||||
|
||||
def check_next_docstatus(self):
|
||||
submit_rv = frappe.db.sql(
|
||||
"""select t1.name
|
||||
from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2
|
||||
where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""",
|
||||
(self.name),
|
||||
)
|
||||
if submit_rv:
|
||||
frappe.throw(_("Purchase Invoice {0} is already submitted").format(self.submit_rv[0][0]))
|
||||
|
||||
def on_cancel(self):
|
||||
super().on_cancel()
|
||||
|
||||
self.check_on_hold_or_closed_status()
|
||||
# Check if Purchase Invoice has been submitted against current Purchase Order
|
||||
submitted = frappe.db.sql(
|
||||
"""select t1.name
|
||||
from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2
|
||||
where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""",
|
||||
self.name,
|
||||
)
|
||||
if submitted:
|
||||
frappe.throw(_("Purchase Invoice {0} is already submitted").format(submitted[0][0]))
|
||||
|
||||
self.update_prevdoc_status()
|
||||
self.update_billing_status()
|
||||
|
||||
@@ -3263,14 +3263,11 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled("_Test Company")
|
||||
frappe.local.enable_perpetual_inventory["_Test Company"] = 1
|
||||
old_inventory_account = frappe.db.get_value("Company", "_Test Company", "default_inventory_account")
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
"_Test Company",
|
||||
{
|
||||
"stock_received_but_not_billed": "Stock Received But Not Billed - _TC",
|
||||
"default_inventory_account": "Stock In Hand - _TC",
|
||||
},
|
||||
"stock_received_but_not_billed",
|
||||
"Stock Received But Not Billed - _TC",
|
||||
)
|
||||
|
||||
pr = make_purchase_receipt(qty=10, rate=1000, do_not_submit=1)
|
||||
@@ -3299,14 +3296,13 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True, as_dict=False)
|
||||
warehouse_account = get_warehouse_account_map("_Test Company")
|
||||
expected_gle = (
|
||||
("Stock Received But Not Billed - _TC", 0.0, 10000.0, "Main - _TC"),
|
||||
("Freight and Forwarding Charges - _TC", 0.0, 2000.0, "Main - _TC"),
|
||||
("Expenses Included In Valuation - _TC", 0.0, 2000.0, "Main - _TC"),
|
||||
(warehouse_account[pr.items[0].warehouse]["account"], 14000.0, 0.0, "Main - _TC"),
|
||||
("Stock Received But Not Billed - _TC", 0, 10000, "Main - _TC"),
|
||||
("Freight and Forwarding Charges - _TC", 0, 2000, "Main - _TC"),
|
||||
("Expenses Included In Valuation - _TC", 0, 2000, "Main - _TC"),
|
||||
(warehouse_account[pr.items[0].warehouse]["account"], 14000, 0, "Main - _TC"),
|
||||
)
|
||||
self.assertCountEqual(expected_gle, gl_entries)
|
||||
self.assertSequenceEqual(expected_gle, gl_entries)
|
||||
frappe.local.enable_perpetual_inventory["_Test Company"] = old_perpetual_inventory
|
||||
frappe.db.set_value("Company", "_Test Company", "default_inventory_account", old_inventory_account)
|
||||
|
||||
def test_manufacturing_and_expiry_date_for_batch(self):
|
||||
item = make_item(
|
||||
@@ -5450,32 +5446,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account)
|
||||
self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2)
|
||||
|
||||
def test_cancel_blocked_by_submitted_invoice_rolls_back(self):
|
||||
"""A submitted Purchase Invoice must block cancelling its Purchase Receipt. Frappe's backlink
|
||||
check rejects the cancel only after on_cancel has run stock, GL, and status work, so the whole
|
||||
transaction has to roll back: the receipt stays submitted with no leaked ledger entries."""
|
||||
pr = make_purchase_receipt()
|
||||
pi = make_purchase_invoice(pr.name)
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
pr.reload()
|
||||
status_before = pr.status
|
||||
sle_before = frappe.db.count("Stock Ledger Entry", {"voucher_no": pr.name})
|
||||
gle_before = frappe.db.count("GL Entry", {"voucher_no": pr.name})
|
||||
|
||||
frappe.db.savepoint("before_blocked_cancel")
|
||||
with self.assertRaises(frappe.LinkExistsError) as cm:
|
||||
pr.cancel()
|
||||
self.assertIn(pi.name, str(cm.exception))
|
||||
frappe.db.rollback(save_point="before_blocked_cancel") # mimic the request-level rollback
|
||||
|
||||
pr.reload()
|
||||
self.assertEqual(pr.docstatus, 1)
|
||||
self.assertEqual(pr.status, status_before)
|
||||
self.assertEqual(frappe.db.count("Stock Ledger Entry", {"voucher_no": pr.name}), sle_before)
|
||||
self.assertEqual(frappe.db.count("GL Entry", {"voucher_no": pr.name}), gle_before)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -505,11 +505,6 @@ class SerialandBatchBundle(Document):
|
||||
self.child_table, self.voucher_detail_no, field
|
||||
)
|
||||
|
||||
if not return_against_voucher_detail_no and self.voucher_type in ("Delivery Note", "Sales Invoice"):
|
||||
# Bundles built via the use_serial_batch_fields / SLE-driven path keep the Packed Item
|
||||
# as voucher_detail_no (not remapped to the DN/SI Item), so the lookup above misses.
|
||||
return_against_voucher_detail_no = self.get_return_against_packed_item(field)
|
||||
|
||||
filters = [
|
||||
["Serial and Batch Bundle", "voucher_no", "=", return_against],
|
||||
["Serial and Batch Entry", "docstatus", "=", 1],
|
||||
@@ -553,16 +548,6 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
return valuation_details
|
||||
|
||||
def get_return_against_packed_item(self, field):
|
||||
"""Resolve the original DN/SI Item when a return bundle's voucher_detail_no is the Packed Item."""
|
||||
parent_detail_docname = frappe.db.get_value(
|
||||
"Packed Item", self.voucher_detail_no, "parent_detail_docname"
|
||||
)
|
||||
if not parent_detail_docname:
|
||||
return
|
||||
|
||||
return frappe.db.get_value(self.child_table, parent_detail_docname, field)
|
||||
|
||||
def get_legacy_valuation_rate_for_return_entry(
|
||||
self, return_against, return_against_voucher_detail_no, return_warehouse=None
|
||||
):
|
||||
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, add_to_date, flt, nowtime, today
|
||||
from frappe.utils import flt, nowtime, today
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
@@ -1512,186 +1512,3 @@ class TestSerialandBatchBundleLogic(FrappeTestCase):
|
||||
|
||||
self.assertNotIn(bundles[1], bundle_wise_serial_nos)
|
||||
self.assertEqual(bundle_wise_serial_nos[bundles[0]], [serial_no])
|
||||
|
||||
@change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1})
|
||||
def test_batchwise_valuation_for_same_posting_datetime_entries(self):
|
||||
# an inward at a different rate and multiple outward rows with the same
|
||||
# item and warehouse share the same posting datetime, the tie-breaking
|
||||
# must include the same-timestamp entries which are already part of the
|
||||
# ledger and must not let the outward rows count each other
|
||||
item_code = make_item(
|
||||
"Test Batchwise Same Posting Datetime Item 1",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBSPD-ITEM1-.#####",
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
receipt = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
target=warehouse,
|
||||
posting_date=add_days(today(), -5),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
|
||||
self.assertTrue(frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"))
|
||||
|
||||
# same posting datetime as the outward rows below, at a different rate
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=20,
|
||||
rate=250,
|
||||
target=warehouse,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
issue = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=2,
|
||||
source=warehouse,
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="12:00:00",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
for qty in [3, 4]:
|
||||
issue.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"s_warehouse": warehouse,
|
||||
"qty": qty,
|
||||
"conversion_factor": 1,
|
||||
},
|
||||
)
|
||||
|
||||
issue.save()
|
||||
issue.submit()
|
||||
|
||||
# (10 * 100 + 20 * 250) / 30 = 200
|
||||
self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=200.0, balance_value=4200.0)
|
||||
|
||||
# backdated receipt reposts the same posting datetime cluster
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
target=warehouse,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
posting_date=add_days(today(), -4),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
# (20 * 100 + 20 * 250) / 40 = 175
|
||||
self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=175.0, balance_value=5425.0)
|
||||
|
||||
@change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1})
|
||||
def test_batchwise_valuation_when_bundle_created_before_the_sle(self):
|
||||
# a bundle can be created (drafted) much before / after its SLE, the
|
||||
# tie-breaking for the same posting datetime entries must follow the
|
||||
# SLE creation and not the bundle creation
|
||||
item_code = make_item(
|
||||
"Test Batchwise Same Posting Datetime Item 2",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBSPD-ITEM2-.#####",
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
receipt = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
target=warehouse,
|
||||
posting_date=add_days(today(), -5),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
|
||||
|
||||
# inward at a different rate, same posting datetime as the outward below
|
||||
inward = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=200,
|
||||
target=warehouse,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
outward = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
source=warehouse,
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
# simulate the inward's bundle drafted after the outward's SLE, the
|
||||
# bundle creation timeline no longer matches the SLE creation timeline
|
||||
outward_sle_creation = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": outward.name, "is_cancelled": 0},
|
||||
"creation",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Serial and Batch Bundle",
|
||||
inward.items[0].serial_and_batch_bundle,
|
||||
"creation",
|
||||
add_to_date(outward_sle_creation, minutes=30),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
repost = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"based_on": "Item and Warehouse",
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": add_days(today(), -6),
|
||||
"posting_time": "00:00:00",
|
||||
"allow_negative_stock": 1,
|
||||
}
|
||||
)
|
||||
|
||||
repost.submit()
|
||||
|
||||
# (10 * 100 + 10 * 200) / 20 = 150, the inward precedes the outward as
|
||||
# per the SLE creation even though its bundle was created afterwards
|
||||
self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=150.0, balance_value=1500.0)
|
||||
|
||||
def assert_batchwise_outgoing_rate(self, item_code, outgoing_rate, balance_value):
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"item_code": item_code, "is_cancelled": 0},
|
||||
fields=["actual_qty", "stock_value_difference", "stock_value"],
|
||||
order_by="posting_datetime, creation",
|
||||
)
|
||||
|
||||
for sle in sl_entries:
|
||||
if sle.actual_qty > 0:
|
||||
continue
|
||||
|
||||
self.assertEqual(flt(sle.stock_value_difference, 2), flt(sle.actual_qty * outgoing_rate, 2))
|
||||
|
||||
self.assertEqual(flt(sl_entries[-1].stock_value, 2), flt(balance_value, 2))
|
||||
|
||||
@@ -1222,11 +1222,9 @@ class StockEntry(StockController):
|
||||
first_row_by_item.setdefault(key, item)
|
||||
|
||||
for key, transfer_qty in transfer_by_item.items():
|
||||
item = first_row_by_item[key]
|
||||
precision = item.precision("qty")
|
||||
transfer_qty = flt(transfer_qty, precision)
|
||||
pending_qty = max(0.0, flt(pending_by_item[key], precision))
|
||||
pending_qty = max(0.0, pending_by_item[key])
|
||||
if transfer_qty > pending_qty:
|
||||
item = first_row_by_item[key]
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot transfer {1} {2} of Item {3}. "
|
||||
|
||||
@@ -934,38 +934,6 @@ class TestStockEntry(FrappeTestCase):
|
||||
fg_cost = next(filter(lambda x: x.item_code == "_Test FG Item 2", stock_entry.get("items"))).amount
|
||||
self.assertEqual(fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2))
|
||||
|
||||
@change_settings("System Settings", {"float_precision": 3})
|
||||
@change_settings("Manufacturing Settings", {"backflush_raw_materials_based_on": "BOM"})
|
||||
def test_material_transfer_for_manufacture_qty_precision(self):
|
||||
work_order = frappe.new_doc("Work Order")
|
||||
work_order.append(
|
||||
"required_items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"required_qty": 33.876,
|
||||
"transferred_qty": 33.875,
|
||||
},
|
||||
)
|
||||
|
||||
stock_entry = frappe.new_doc("Stock Entry")
|
||||
stock_entry.work_order = "Test Work Order"
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"s_warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 0.001,
|
||||
"uom": "Nos",
|
||||
},
|
||||
)
|
||||
|
||||
stock_entry.pro_doc = work_order
|
||||
stock_entry._validate_no_excess_transfer()
|
||||
|
||||
stock_entry.items[0].qty = 0.002
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
stock_entry._validate_no_excess_transfer()
|
||||
|
||||
@change_settings("Manufacturing Settings", {"material_consumption": 1})
|
||||
def test_work_order_manufacture_with_material_consumption(self):
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
|
||||
@@ -1292,7 +1292,6 @@ def get_row_stock_value_difference(voucher_type: str, voucher_no: str, voucher_d
|
||||
return flt(result[0][0]) if result and result[0][0] else 0.0
|
||||
|
||||
|
||||
# nosemgrep: missing-argument-type-hint
|
||||
@frappe.whitelist()
|
||||
def get_stock_balance_for(
|
||||
item_code: str,
|
||||
@@ -1365,7 +1364,7 @@ def get_stock_balance_for(
|
||||
or 0
|
||||
)
|
||||
|
||||
if row and row.use_serial_batch_fields and row.batch_no and (qty or row.current_qty):
|
||||
if row.use_serial_batch_fields and row.batch_no and (qty or row.current_qty):
|
||||
rate = get_incoming_rate(
|
||||
frappe._dict(
|
||||
{
|
||||
|
||||
@@ -103,44 +103,6 @@ class TestWarehouse(FrappeTestCase):
|
||||
children = get_children("Warehouse", parent=company, company=company, is_root=True)
|
||||
self.assertTrue(any(wh["value"] == "_Test Warehouse - _TC" for wh in children))
|
||||
|
||||
def test_inventory_account_fallback_with_multiple_stock_accounts(self):
|
||||
from erpnext.stock import get_warehouse_account
|
||||
|
||||
company = create_inventory_fallback_company()
|
||||
frappe.db.set_value("Company", company, "default_inventory_account", None)
|
||||
if frappe.db.exists("Account", "Extra Inventory Account - _TCIF"):
|
||||
frappe.delete_doc("Account", "Extra Inventory Account - _TCIF")
|
||||
|
||||
warehouse = frappe.get_doc("Warehouse", {"company": company, "is_group": 0})
|
||||
single_account = frappe.db.get_value(
|
||||
"Account", {"account_type": "Stock", "is_group": 0, "company": company}, "name"
|
||||
)
|
||||
self.assertEqual(get_warehouse_account(warehouse), single_account)
|
||||
|
||||
create_account(
|
||||
account_name="Extra Inventory Account",
|
||||
parent_account=frappe.db.get_value("Account", single_account, "parent_account"),
|
||||
account_type="Stock",
|
||||
company=company,
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, get_warehouse_account, warehouse)
|
||||
|
||||
|
||||
def create_inventory_fallback_company():
|
||||
company = "_Test Company Inventory Fallback"
|
||||
if not frappe.db.exists("Company", company):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": company,
|
||||
"abbr": "_TCIF",
|
||||
"default_currency": "INR",
|
||||
"enable_perpetual_inventory": 0,
|
||||
"country": "India",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
return company
|
||||
|
||||
|
||||
def create_warehouse(warehouse_name, properties=None, company=None):
|
||||
if not company:
|
||||
|
||||
@@ -829,43 +829,14 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
child = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
sle_creation = self.sle.creation if self.sle.get("name") else None
|
||||
if not self.sle.get("name") and self.sle.get("serial_and_batch_bundle"):
|
||||
sle_creation = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"serial_and_batch_bundle": self.sle.serial_and_batch_bundle, "is_cancelled": 0},
|
||||
"creation",
|
||||
)
|
||||
|
||||
timestamp_condition = ""
|
||||
if self.sle.posting_datetime:
|
||||
timestamp_condition = parent.posting_datetime < self.sle.posting_datetime
|
||||
|
||||
sle_table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
if sle_creation:
|
||||
# bundle creation and SLE creation are different timelines (a
|
||||
# bundle can be created much before its SLE), so break the tie
|
||||
# using the creation of the bundle's own SLE
|
||||
tie_condition = ExistsCriterion(
|
||||
frappe.qb.from_(sle_table)
|
||||
.select(sle_table.name)
|
||||
.where(
|
||||
(sle_table.serial_and_batch_bundle == parent.name)
|
||||
& (sle_table.is_cancelled == 0)
|
||||
& (sle_table.creation < sle_creation)
|
||||
)
|
||||
if self.sle.creation:
|
||||
timestamp_condition |= (parent.posting_datetime == self.sle.posting_datetime) & (
|
||||
parent.creation < self.sle.creation
|
||||
)
|
||||
else:
|
||||
# the current entry is not yet in the ledger and will get the
|
||||
# latest creation, so the same-timestamp entries which are
|
||||
# already in the ledger precede it
|
||||
tie_condition = ExistsCriterion(
|
||||
frappe.qb.from_(sle_table)
|
||||
.select(sle_table.name)
|
||||
.where((sle_table.serial_and_batch_bundle == parent.name) & (sle_table.is_cancelled == 0))
|
||||
)
|
||||
|
||||
timestamp_condition |= (parent.posting_datetime == self.sle.posting_datetime) & tie_condition
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
|
||||
Reference in New Issue
Block a user