Compare commits

..

1 Commits

Author SHA1 Message Date
Richard Case
bbd718d98d fix: Production Plan: load document defaults for plan items & remove name column from listview (#26584)
(cherry picked from commit 69d88a9212)

# Conflicts:
#	erpnext/manufacturing/doctype/production_plan/production_plan.py
#	erpnext/manufacturing/doctype/production_plan/production_plan_list.js
2024-11-19 00:13:42 +00:00
140 changed files with 11548 additions and 14732 deletions

View File

@@ -38,7 +38,3 @@ ec74a5e56617bbd76ac402451468fd4668af543d
# ruff formatting
a308792ee7fda18a681e9181f4fd00b36385bc23
# noisy typing refactoring of get_item_details
7b7211ac79c248a79ba8a999ff34e734d874c0ae
d827ed21adc7b36047e247cbb0dc6388d048a7f9

View File

@@ -3,11 +3,11 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @ruthra-kumar
erpnext/assets/ @khushi8112
erpnext/regional @ruthra-kumar
erpnext/selling @ruthra-kumar
erpnext/support/ @ruthra-kumar
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @khushi8112 @deepeshgarg007
erpnext/regional @deepeshgarg007 @ruthra-kumar
erpnext/selling @deepeshgarg007 @ruthra-kumar
erpnext/support/ @deepeshgarg007
pos*
erpnext/buying/ @rohitwaghchaure
@@ -17,8 +17,8 @@ erpnext/quality_management/ @rohitwaghchaure
erpnext/stock/ @rohitwaghchaure
erpnext/subcontracting @rohitwaghchaure
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure
erpnext/patches/ @ruthra-kumar
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
erpnext/patches/ @deepeshgarg007
.github/ @ruthra-kumar
.github/ @deepeshgarg007
pyproject.toml @akhilnarang

View File

@@ -1,9 +1,7 @@
import functools
import inspect
from typing import TypeVar
import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "16.0.0-dev"
@@ -162,34 +160,3 @@ def check_app_permission():
return False
return True
T = TypeVar("T")
def normalize_ctx_input(T: type) -> callable:
"""
Normalizes the first argument (ctx) of the decorated function by:
- Converting Document objects to dictionaries
- Parsing JSON strings
- Casting the result to the specified type T
"""
def decorator(func: callable):
# conserve annotations for frappe.utils.typing_validations
@functools.wraps(func, assigned=(a for a in functools.WRAPPER_ASSIGNMENTS if a != "__annotations__"))
def wrapper(ctx: T | Document | dict | str, *args, **kwargs):
if isinstance(ctx, Document):
ctx = T(**ctx.as_dict())
elif isinstance(ctx, dict):
ctx = T(**ctx)
else:
ctx = T(**frappe.parse_json(ctx))
return func(ctx, *args, **kwargs)
# set annotations from function
wrapper.__annotations__.update({k: v for k, v in func.__annotations__.items() if k != "ctx"})
return wrapper
return decorator

View File

@@ -13,7 +13,6 @@
"col_break_1",
"description",
"included_in_paid_amount",
"set_by_item_tax_template",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -184,8 +183,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Net Amount",
"options": "currency",
"read_only": 1
"options": "currency"
},
{
"fieldname": "base_net_amount",
@@ -195,22 +193,12 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"default": "0",
"fieldname": "set_by_item_tax_template",
"fieldtype": "Check",
"hidden": 1,
"label": "Set by Item Tax Template",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-11-22 19:16:22.346267",
"modified": "2024-09-24 06:51:07.417348",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Taxes and Charges",
@@ -219,4 +207,4 @@
"sort_field": "creation",
"sort_order": "ASC",
"states": []
}
}

View File

@@ -34,7 +34,6 @@ class AdvanceTaxesandCharges(Document):
parenttype: DF.Data
rate: DF.Float
row_id: DF.Data | None
set_by_item_tax_template: DF.Check
tax_amount: DF.Currency
total: DF.Currency
# end: auto-generated types

View File

@@ -62,7 +62,7 @@
"unique": 1
},
{
"depends_on": "eval: !doc.from_external_ecomm_platform",
"depends_on": "eval !doc.from_external_ecomm_platform",
"fieldname": "pricing_rule",
"fieldtype": "Link",
"label": "Pricing Rule",
@@ -125,7 +125,7 @@
}
],
"links": [],
"modified": "2024-11-19 16:35:11.836441",
"modified": "2024-06-28 06:17:01.833399",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Coupon Code",
@@ -186,4 +186,4 @@
"states": [],
"title_field": "coupon_name",
"track_changes": 1
}
}

View File

@@ -19,6 +19,16 @@
"currency",
"column_break_11",
"conversion_rate",
"address_and_contact_section",
"customer_address",
"address_display",
"contact_person",
"contact_display",
"column_break_16",
"company_address",
"company_address_display",
"contact_mobile",
"contact_email",
"section_break_6",
"dunning_type",
"column_break_8",
@@ -46,21 +56,7 @@
"income_account",
"column_break_48",
"cost_center",
"amended_from",
"address_and_contact_tab",
"address_and_contact_section",
"customer_address",
"address_display",
"column_break_vodj",
"contact_person",
"contact_display",
"contact_mobile",
"contact_email",
"section_break_xban",
"column_break_16",
"company_address",
"company_address_display",
"column_break_lqmf"
"amended_from"
],
"fields": [
{
@@ -182,8 +178,10 @@
"label": "Rate of Interest (%) Yearly"
},
{
"collapsible": 1,
"fieldname": "address_and_contact_section",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Address and Contact"
},
{
"fieldname": "address_display",
@@ -379,28 +377,11 @@
{
"fieldname": "column_break_48",
"fieldtype": "Column Break"
},
{
"fieldname": "address_and_contact_tab",
"fieldtype": "Tab Break",
"label": "Address & Contact"
},
{
"fieldname": "column_break_vodj",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_xban",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_lqmf",
"fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
"modified": "2024-11-26 13:46:07.760867",
"modified": "2024-03-27 13:08:19.176146",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dunning",

View File

@@ -72,10 +72,10 @@
},
{
"default": "0",
"description": "More/Less than 12 months.",
"description": "Less than 12 months.",
"fieldname": "is_short_year",
"fieldtype": "Check",
"label": "Is Short/Long Year",
"label": "Is Short Year",
"set_only_once": 1
}
],

View File

@@ -523,55 +523,6 @@ class TestJournalEntry(IntegrationTestCase):
self.assertEqual(row.debit_in_account_currency, 100)
self.assertEqual(row.credit_in_account_currency, 100)
def test_toggle_debit_credit_if_negative(self):
from erpnext.accounts.general_ledger import process_gl_map
# Create JV with defaut cost center - _Test Cost Center
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
jv = frappe.new_doc("Journal Entry")
jv.posting_date = nowdate()
jv.company = "_Test Company"
jv.user_remark = "test"
jv.extend(
"accounts",
[
{
"account": "_Test Cash - _TC",
"debit": 100 * -1,
"debit_in_account_currency": 100 * -1,
"exchange_rate": 1,
},
{
"account": "_Test Bank - _TC",
"credit": 100 * -1,
"credit_in_account_currency": 100 * -1,
"exchange_rate": 1,
},
],
)
jv.flags.ignore_validate = True
jv.save()
self.assertEqual(len(jv.accounts), 2)
gl_map = jv.build_gl_map()
for row in gl_map:
if row.account == "_Test Cash - _TC":
self.assertEqual(row.debit, 100 * -1)
self.assertEqual(row.debit_in_account_currency, 100 * -1)
self.assertEqual(row.debit_in_transaction_currency, 100 * -1)
gl_map = process_gl_map(gl_map, False)
for row in gl_map:
if row.account == "_Test Cash - _TC":
self.assertEqual(row.credit, 100)
self.assertEqual(row.credit_in_account_currency, 100)
self.assertEqual(row.credit_in_transaction_currency, 100)
def test_transaction_exchange_rate_on_journals(self):
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False)
jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1})

View File

@@ -185,10 +185,6 @@ frappe.ui.form.on("Payment Entry", {
filters: {
reference_doctype: row.reference_doctype,
reference_name: row.reference_name,
company: doc.company,
status: ["!=", "Paid"],
outstanding_amount: [">", 0], // for compatibility with old data
docstatus: 1,
},
};
});

View File

@@ -1848,7 +1848,7 @@ class PaymentEntry(AccountsController):
if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0:
frappe.msgprint(
_("Cannot {0} from {1} without any negative outstanding invoice").format(
_("Cannot {0} from {2} without any negative outstanding invoice").format(
self.payment_type,
self.party_type,
)

View File

@@ -211,14 +211,12 @@ class PaymentReconciliation(Document):
if self.get("cost_center"):
conditions.append(jea.cost_center == self.cost_center)
account_type = erpnext.get_party_account_type(self.party_type)
if account_type == "Receivable":
dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency
elif account_type == "Payable":
dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency
conditions.append(dr_or_cr.gt(0))
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
conditions.append(jea[dr_or_cr].gt(0))
if self.bank_cash_account:
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
@@ -233,7 +231,7 @@ class PaymentReconciliation(Document):
je.posting_date,
je.remark.as_("remarks"),
jea.name.as_("reference_row"),
dr_or_cr.as_("amount"),
jea[dr_or_cr].as_("amount"),
jea.is_advance,
jea.exchange_rate,
jea.account_currency.as_("currency"),

View File

@@ -641,42 +641,6 @@ class TestPaymentReconciliation(IntegrationTestCase):
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_negative_debit_or_credit_journal_against_invoice(self):
transaction_date = nowdate()
amount = 100
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
# credit debtors account to record a payment
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
je.accounts[1].party_type = "Customer"
je.accounts[1].party = self.customer
je.accounts[1].credit_in_account_currency = 0
je.accounts[1].debit_in_account_currency = -1 * amount
je.save()
je.submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
# assert outstanding
si.reload()
self.assertEqual(si.status, "Paid")
self.assertEqual(si.outstanding_amount, 0)
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_journal_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
@@ -999,100 +963,6 @@ class TestPaymentReconciliation(IntegrationTestCase):
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
)
def test_difference_amount_via_negative_debit_or_credit_journal_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer4
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.save().submit()
# Make payment using Journal Entry
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].credit_in_account_currency = -8000
je1.accounts[0].credit = -8000
je1.accounts[0].debit_in_account_currency = 0
je1.accounts[0].debit = 0
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer4
je1.accounts[1].exchange_rate = 80
je1.accounts[1].credit_in_account_currency = 100
je1.accounts[1].credit = 8000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je1.save()
je1.submit()
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].credit_in_account_currency = -16000
je2.accounts[0].credit = -16000
je2.accounts[0].debit_in_account_currency = 0
je2.accounts[0].debit = 0
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer4
je2.accounts[1].exchange_rate = 80
je2.accounts[1].credit_in_account_currency = 200
je1.accounts[1].credit = 16000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je2.save()
je2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer4
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
# Test exact payment allocation
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Test partial payment allocation (with excess payment entry)
pr.set("allocation", [])
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Check if difference journal entry gets generated for difference amount after reconciliation
pr.reconcile()
total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(credit) as amount",
group_by="reference_name",
)[0].amount
# total credit includes the exchange gain/loss amount
self.assertEqual(flt(total_credit_amount, 2), 8500)
jea_parent = frappe.db.get_all(
"Journal Entry Account",
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
fields=["parent"],
)[0]
self.assertEqual(
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
)
def test_difference_amount_via_payment_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(

View File

@@ -945,17 +945,21 @@ def validate_payment(doc, method=None):
@frappe.whitelist()
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
# permission checks in `get_list()`
filters = frappe._dict(filters)
reference_doctype = filters.get("reference_doctype")
reference_name = filters.get("reference_doctype")
if not filters.reference_doctype or not filters.reference_name:
if not reference_doctype or not reference_name:
return []
if txt:
filters.name = ["like", f"%{txt}%"]
open_payment_requests = frappe.get_list(
"Payment Request",
filters=filters,
filters={
"reference_doctype": filters["reference_doctype"],
"reference_name": filters["reference_name"],
"status": ["!=", "Paid"],
"outstanding_amount": ["!=", 0], # for compatibility with old data
"docstatus": 1,
},
fields=["name", "grand_total", "outstanding_amount"],
order_by="transaction_date ASC,creation ASC",
)

View File

@@ -1,14 +0,0 @@
from frappe import _
def get_data():
return {
"fieldname": "payment_request",
"internal_links": {
"Payment Entry": ["references", "payment_request"],
"Payment Order": ["references", "payment_order"],
},
"transactions": [
{"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]},
],
}

View File

@@ -1,18 +1,21 @@
const INDICATORS = {
"Partially Paid": "orange",
Cancelled: "red",
Draft: "gray",
Failed: "red",
Initiated: "green",
Paid: "blue",
Requested: "green",
};
frappe.listview_settings["Payment Request"] = {
add_fields: ["status"],
get_indicator: function (doc) {
if (!doc.status || !INDICATORS[doc.status]) return;
return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`];
if (doc.status == "Draft") {
return [__("Draft"), "gray", "status,=,Draft"];
}
if (doc.status == "Requested") {
return [__("Requested"), "green", "status,=,Requested"];
} else if (doc.status == "Initiated") {
return [__("Initiated"), "green", "status,=,Initiated"];
} else if (doc.status == "Partially Paid") {
return [__("Partially Paid"), "orange", "status,=,Partially Paid"];
} else if (doc.status == "Paid") {
return [__("Paid"), "blue", "status,=,Paid"];
} else if (doc.status == "Failed") {
return [__("Failed"), "red", "status,=,Failed"];
} else if (doc.status == "Cancelled") {
return [__("Cancelled"), "red", "status,=,Cancelled"];
}
},
};

View File

@@ -218,25 +218,11 @@ class TestPOSClosingEntry(IntegrationTestCase):
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv = create_pos_invoice(
item_code=item_code,
qty=5,
rate=300,
use_serial_batch_fields=1,
batch_no=batch_no,
do_not_submit=True,
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
)
pos_inv.payments[0].amount = pos_inv.grand_total
pos_inv.submit()
pos_inv2 = create_pos_invoice(
item_code=item_code,
qty=5,
rate=300,
use_serial_batch_fields=1,
batch_no=batch_no,
do_not_submit=True,
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
)
pos_inv2.payments[0].amount = pos_inv2.grand_total
pos_inv2.submit()
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
self.assertEqual(batch_qty, 10)

View File

@@ -240,7 +240,6 @@ class POSInvoice(SalesInvoice):
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
update_coupon_code_count(self.coupon_code, "used")
self.clear_unallocated_mode_of_payments()
def before_cancel(self):
if (
@@ -279,12 +278,6 @@ class POSInvoice(SalesInvoice):
self.delink_serial_and_batch_bundle()
def clear_unallocated_mode_of_payments(self):
self.set("payments", self.get("payments", {"amount": ["not in", [0, None, ""]]}))
sip = frappe.qb.DocType("Sales Invoice Payment")
frappe.qb.from_(sip).delete().where(sip.parent == self.name).where(sip.amount == 0).run()
def delink_serial_and_batch_bundle(self):
for row in self.items:
if row.serial_and_batch_bundle:
@@ -540,11 +533,7 @@ class POSInvoice(SalesInvoice):
def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles"""
from erpnext.stock.get_item_details import (
ItemDetailsCtx,
get_pos_profile,
get_pos_profile_item_details_,
)
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {}
@@ -613,8 +602,8 @@ class POSInvoice(SalesInvoice):
# set pos values in items
for item in self.get("items"):
if item.get("item_code"):
profile_details = get_pos_profile_item_details_(
ItemDetailsCtx(item.as_dict()), profile.get("company"), profile
profile_details = get_pos_profile_item_details(
profile.get("company"), frappe._dict(item.as_dict()), profile
)
for fname, val in profile_details.items():
if (not for_validate) or (for_validate and not item.get(fname)):

View File

@@ -134,7 +134,6 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
},
)
inv.insert()
inv.payments[0].amount = inv.grand_total
inv.submit()
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
@@ -151,7 +150,6 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
},
)
inv2.insert()
inv2.payments[0].amount = inv.grand_total
inv2.submit()
consolidate_pos_invoices()
@@ -159,19 +157,16 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
item_wise_tax_detail = json.loads(consolidated_invoice.get("taxes")[0].item_wise_tax_detail)
expected_item_wise_tax_detail = {
"_Test Item": {
"tax_rate": 9,
"tax_amount": 9,
"net_amount": 100,
},
"_Test Item 2": {
"tax_rate": 5,
"tax_amount": 5,
"net_amount": 100,
},
}
self.assertEqual(item_wise_tax_detail, expected_item_wise_tax_detail)
tax_data = item_wise_tax_detail.get("_Test Item")
self.assertEqual(tax_data.get("tax_rate"), 9)
self.assertEqual(tax_data.get("tax_amount"), 9)
self.assertEqual(tax_data.get("net_amount"), 100)
tax_data = item_wise_tax_detail.get("_Test Item 2")
self.assertEqual(tax_data.get("tax_rate"), 5)
self.assertEqual(tax_data.get("tax_amount"), 5)
self.assertEqual(tax_data.get("net_amount"), 100)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")

View File

@@ -451,16 +451,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
)
if pricing_rule.coupon_code_based == 1:
if not args.coupon_code:
return item_details
coupon_code = frappe.db.get_value(
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
)
if args.coupon_code != coupon_code:
continue
if pricing_rule.get("suggestion"):
continue
@@ -486,6 +476,9 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
pricing_rule.apply_rule_on_other_items
)
if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
return item_details
if not pricing_rule.validate_applied_rule:
if pricing_rule.price_or_product_discount == "Price":
apply_price_discount_rule(pricing_rule, item_details, args)

View File

@@ -1147,45 +1147,6 @@ class TestPricingRule(IntegrationTestCase):
so.save()
self.assertEqual(len(so.items), 1)
def test_pricing_rule_for_product_free_item_round_free_qty(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"items": [
{
"item_code": "_Test Item",
}
],
"selling": 1,
"rate": 0,
"min_qty": 100,
"max_qty": 0,
"price_or_product_discount": "Product",
"same_item": 1,
"free_qty": 10,
"round_free_qty": 1,
"is_recursive": 1,
"recurse_for": 100,
"company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
# With pricing rule
so = make_sales_order(item_code="_Test Item", qty=100)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 10)
so = make_sales_order(item_code="_Test Item", qty=150)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 10)
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")

View File

@@ -655,7 +655,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if transaction_qty:
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
if pricing_rule.round_free_qty:
qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
qty = math.floor(qty)
if not qty:
return

View File

@@ -474,7 +474,6 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
reference_doctype="Process Statement Of Accounts",
reference_name=document_name,
attachments=attachments,
expose_recipients="header",
)
if doc.enable_auto_email and from_scheduler:

View File

@@ -17,7 +17,6 @@
"account_head",
"description",
"is_tax_withholding_account",
"set_by_item_tax_template",
"section_break_10",
"rate",
"accounting_dimensions_section",
@@ -245,8 +244,7 @@
"label": "Net Amount",
"oldfieldname": "tax_amount",
"oldfieldtype": "Currency",
"options": "currency",
"read_only": 1
"options": "currency"
},
{
"fieldname": "base_net_amount",
@@ -255,22 +253,12 @@
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "set_by_item_tax_template",
"fieldtype": "Check",
"hidden": 1,
"label": "Set by Item Tax Template",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-11-22 19:17:02.377473",
"modified": "2024-09-24 06:47:25.129901",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",

View File

@@ -42,7 +42,6 @@ class PurchaseTaxesandCharges(Document):
parenttype: DF.Data
rate: DF.Float
row_id: DF.Data | None
set_by_item_tax_template: DF.Check
tax_amount: DF.Currency
tax_amount_after_discount_amount: DF.Currency
total: DF.Currency

View File

@@ -45,7 +45,7 @@ class RepostAccountingLedger(Document):
latest_pcv = (
frappe.db.get_all(
"Period Closing Voucher",
filters={"company": self.company, "docstatus": 1},
filters={"company": self.company},
order_by="period_end_date desc",
pluck="period_end_date",
limit=1,

View File

@@ -740,6 +740,20 @@ frappe.ui.form.on("Sales Invoice", {
};
});
frm.set_query("company_address", function (doc) {
if (!doc.company) {
frappe.throw(__("Please set Company"));
}
return {
query: "frappe.contacts.doctype.address.address.address_query",
filters: {
link_doctype: "Company",
link_name: doc.company,
},
};
});
frm.set_query("pos_profile", function (doc) {
if (!doc.company) {
frappe.throw(__("Please set Company"));

View File

@@ -766,11 +766,7 @@ class SalesInvoice(SellingController):
"Company", self.company, "default_cash_account"
)
from erpnext.stock.get_item_details import (
ItemDetailsCtx,
get_pos_profile,
get_pos_profile_item_details_,
)
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
if not self.pos_profile and not self.flags.ignore_pos_profile:
pos_profile = get_pos_profile(self.company) or {}
@@ -838,8 +834,8 @@ class SalesInvoice(SellingController):
# set pos values in items
for item in self.get("items"):
if item.get("item_code"):
profile_details = get_pos_profile_item_details_(
ItemDetailsCtx(item.as_dict()), pos, pos, update_data=True
profile_details = get_pos_profile_item_details(
pos, frappe._dict(item.as_dict()), pos, update_data=True
)
for fname, val in profile_details.items():
if (not for_validate) or (for_validate and not item.get(fname)):

View File

@@ -90,14 +90,11 @@
"incoming_rate",
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
"section_break_eoec",
"serial_no",
"column_break_ytgd",
"batch_no",
"available_quantity_section",
"actual_qty",
"column_break_ogff",
"company_total_stock",
"edit_references",
"sales_order",
"so_detail",
@@ -679,8 +676,7 @@
"allow_on_submit": 1,
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": "Qty (Warehouse)",
"no_copy": 1,
"label": "Available Qty at Warehouse",
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
@@ -934,30 +930,12 @@
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Quantity"
},
{
"fieldname": "column_break_ogff",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Qty (Company)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-11-25 16:27:33.287341",
"modified": "2024-10-28 15:06:40.980995",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -34,7 +34,6 @@ class SalesInvoiceItem(Document):
base_rate_with_margin: DF.Currency
batch_no: DF.Link | None
brand: DF.Data | None
company_total_stock: DF.Float
conversion_factor: DF.Float
cost_center: DF.Link
customer_item_code: DF.Data | None

View File

@@ -13,7 +13,6 @@
"description",
"included_in_print_rate",
"included_in_paid_amount",
"set_by_item_tax_template",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -222,8 +221,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Net Amount",
"options": "currency",
"read_only": 1
"options": "currency"
},
{
"fieldname": "base_net_amount",
@@ -233,23 +231,13 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"default": "0",
"fieldname": "set_by_item_tax_template",
"fieldtype": "Check",
"hidden": 1,
"label": "Set by Item Tax Template",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-11-22 19:17:31.898467",
"modified": "2024-09-24 06:49:32.034074",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges",

View File

@@ -40,7 +40,6 @@ class SalesTaxesandCharges(Document):
parenttype: DF.Data
rate: DF.Float
row_id: DF.Data | None
set_by_item_tax_template: DF.Check
tax_amount: DF.Currency
tax_amount_after_discount_amount: DF.Currency
total: DF.Currency

View File

@@ -568,7 +568,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
tax_details.tax_on_excess_amount
):
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
supp_credit_amt = net_total - cumulative_threshold
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
tds_amount = get_lower_deduction_amount(

View File

@@ -177,45 +177,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
for d in reversed(invoices):
d.cancel()
def test_cumulative_threshold_with_tax_on_excess_amount(self):
invoices = []
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
# Invoice with tax and without exceeding single and cumulative thresholds
for _ in range(2):
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=10000, do_not_save=True)
pi.apply_tds = 1
pi.append(
"taxes",
{
"category": "Total",
"charge_type": "Actual",
"account_head": "_Test Account VAT - _TC",
"cost_center": "Main - _TC",
"tax_amount": 500,
"description": "Test",
"add_deduct_tax": "Add",
},
)
pi.save()
pi.submit()
invoices.append(pi)
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000)
pi1.apply_tds = 1
pi1.save()
pi1.submit()
invoices.append(pi1)
# Cumulative threshold is 10,000
# Threshold calculation should be only on the third invoice
self.assertTrue(len(pi1.taxes) > 0)
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
for d in reversed(invoices):
d.cancel()
def test_cumulative_threshold_tcs(self):
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"

View File

@@ -316,48 +316,66 @@ def check_if_in_list(gle, gl_map):
def toggle_debit_credit_if_negative(gl_map):
debit_credit_field_map = {
"debit": "credit",
"debit_in_account_currency": "credit_in_account_currency",
"debit_in_transaction_currency": "credit_in_transaction_currency",
}
for entry in gl_map:
# toggle debit, credit if negative entry
for debit_field, credit_field in debit_credit_field_map.items():
debit = flt(entry.get(debit_field))
credit = flt(entry.get(credit_field))
if flt(entry.debit) < 0 and flt(entry.credit) < 0 and flt(entry.debit) == flt(entry.credit):
entry.credit *= -1
entry.debit *= -1
if debit < 0 and credit < 0 and debit == credit:
debit *= -1
credit *= -1
if (
flt(entry.debit_in_account_currency) < 0
and flt(entry.credit_in_account_currency) < 0
and flt(entry.debit_in_account_currency) == flt(entry.credit_in_account_currency)
):
entry.credit_in_account_currency *= -1
entry.debit_in_account_currency *= -1
if debit < 0:
credit = credit - debit
debit = 0.0
if flt(entry.debit) < 0:
entry.credit = flt(entry.credit) - flt(entry.debit)
entry.debit = 0.0
if credit < 0:
debit = debit - credit
credit = 0.0
if flt(entry.debit_in_account_currency) < 0:
entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt(
entry.debit_in_account_currency
)
entry.debit_in_account_currency = 0.0
# update net values
# In some scenarios net value needs to be shown in the ledger
# This method updates net values as debit or credit
if entry.post_net_value and debit and credit:
if debit > credit:
debit = debit - credit
credit = 0.0
if flt(entry.credit) < 0:
entry.debit = flt(entry.debit) - flt(entry.credit)
entry.credit = 0.0
else:
credit = credit - debit
debit = 0.0
if flt(entry.credit_in_account_currency) < 0:
entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt(
entry.credit_in_account_currency
)
entry.credit_in_account_currency = 0.0
entry[debit_field] = debit
entry[credit_field] = credit
update_net_values(entry)
return gl_map
def update_net_values(entry):
# In some scenarios net value needs to be shown in the ledger
# This method updates net values as debit or credit
if entry.post_net_value and entry.debit and entry.credit:
if entry.debit > entry.credit:
entry.debit = entry.debit - entry.credit
entry.debit_in_account_currency = (
entry.debit_in_account_currency - entry.credit_in_account_currency
)
entry.credit = 0
entry.credit_in_account_currency = 0
else:
entry.credit = entry.credit - entry.debit
entry.credit_in_account_currency = (
entry.credit_in_account_currency - entry.debit_in_account_currency
)
entry.debit = 0
entry.debit_in_account_currency = 0
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if not from_repost:
validate_cwip_accounts(gl_map)

View File

@@ -4,14 +4,13 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Purchase Invoice",
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Incoming Bills",
"modified": "2024-11-20 19:08:37.043777",
"modified": "2020-07-22 13:06:46.045344",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Total Incoming Bills",

View File

@@ -4,7 +4,6 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Payment Entry",
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]",
"function": "Sum",
"idx": 0,

View File

@@ -4,7 +4,6 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Sales Invoice",
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
"function": "Sum",
"idx": 0,

View File

@@ -4,7 +4,6 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Payment Entry",
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]",
"function": "Sum",
"idx": 0,

View File

@@ -1013,15 +1013,15 @@ class ReceivablePayableReport:
def get_columns(self):
self.columns = []
self.add_column(_("Posting Date"), fieldtype="Date")
self.add_column("Posting Date", fieldtype="Date")
self.add_column(
label=_("Party Type"),
label="Party Type",
fieldname="party_type",
fieldtype="Data",
width=100,
)
self.add_column(
label=_("Party"),
label="Party",
fieldname="party",
fieldtype="Dynamic Link",
options="party_type",
@@ -1037,10 +1037,10 @@ class ReceivablePayableReport:
if self.party_naming_by == "Naming Series":
if self.account_type == "Payable":
label = _("Supplier Name")
label = "Supplier Name"
fieldname = "supplier_name"
else:
label = _("Customer Name")
label = "Customer Name"
fieldname = "customer_name"
self.add_column(
label=label,
@@ -1066,7 +1066,7 @@ class ReceivablePayableReport:
width=180,
)
self.add_column(label=_("Due Date"), fieldtype="Date")
self.add_column(label="Due Date", fieldtype="Date")
if self.account_type == "Payable":
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")

View File

@@ -263,6 +263,7 @@ def get_report_summary(summary_data, currency):
def get_chart_data(columns, data, currency):
labels = [d.get("label") for d in columns[2:]]
print(data)
datasets = [
{
"name": section.get("section").replace("'", ""),

View File

@@ -619,7 +619,7 @@ def get_cost_centers_with_children(cost_centers):
def get_columns(periodicity, period_list, accumulated_values=1, company=None, cash_flow=False):
columns = [
{
"fieldname": "account",
"fieldname": "stub",
"label": _("Account") if not cash_flow else _("Section"),
"fieldtype": "Link",
"options": "Account",

View File

@@ -2,7 +2,6 @@
# MIT License. See license.txt
import frappe
from frappe.desk.query_report import export_query
from frappe.tests import IntegrationTestCase
from frappe.utils import getdate, today
@@ -91,21 +90,3 @@ class TestProfitAndLossStatement(AccountsTestMixin, IntegrationTestCase):
with self.subTest(current_period_key=current_period_key):
self.assertEqual(acc[current_period_key], 150)
self.assertEqual(acc["total"], 150)
def test_p_and_l_export(self):
self.create_sales_invoice(qty=1, rate=150)
filters = self.get_report_filters()
frappe.local.form_dict = frappe._dict(
{
"report_name": "Profit and Loss Statement",
"file_format_type": "CSV",
"filters": filters,
"visible_idx": [0, 1, 2, 3, 4, 5, 6],
}
)
export_query()
contents = frappe.response["filecontent"].decode()
sales_account = frappe.db.get_value("Company", self.company, "default_income_account")
self.assertIn(sales_account, contents)

View File

@@ -650,16 +650,6 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
if jv_detail.get("reference_type") in advance_payment_doctypes:
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
rev_dr_or_cr = (
"debit_in_account_currency"
if d["dr_or_cr"] == "credit_in_account_currency"
else "credit_in_account_currency"
)
if jv_detail.get(rev_dr_or_cr):
d["dr_or_cr"] = rev_dr_or_cr
d["allocated_amount"] = d["allocated_amount"] * -1
d["unadjusted_amount"] = d["unadjusted_amount"] * -1
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
# adjust the unreconciled balance
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
@@ -1377,14 +1367,6 @@ def parse_naming_series_variable(doc, variable):
company = None
return get_fiscal_year(date=date, company=company)[0]
elif variable == "ABBR":
if doc:
company = doc.get("company") or frappe.db.get_default("company")
else:
company = frappe.db.get_default("company")
return frappe.db.get_value("Company", company, "abbr") if company else ""
@frappe.whitelist()
def get_coa(doctype, parent, is_root=None, chart=None):

View File

@@ -26,10 +26,9 @@ from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.get_item_details import (
ItemDetailsCtx,
get_default_cost_center,
get_default_expense_account,
get_item_warehouse_,
get_item_warehouse,
)
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.stock.utils import get_incoming_rate
@@ -749,7 +748,7 @@ def get_target_item_details(item_code=None, company=None):
item_group_defaults = get_item_group_defaults(item.name, company)
brand_defaults = get_brand_defaults(item.name, company)
out.cost_center = get_default_cost_center(
ItemDetailsCtx({"item_code": item.name, "company": company}),
frappe._dict({"item_code": item.name, "company": company}),
item_defaults,
item_group_defaults,
brand_defaults,
@@ -786,42 +785,45 @@ def get_target_asset_details(asset=None, company=None):
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_consumed_stock_item_details(ctx: ItemDetailsCtx):
def get_consumed_stock_item_details(args):
if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
out = frappe._dict()
item = frappe._dict()
if ctx.item_code:
item = frappe.get_cached_doc("Item", ctx.item_code)
if args.item_code:
item = frappe.get_cached_doc("Item", args.item_code)
out.item_name = item.item_name
out.batch_no = None
out.serial_no = ""
out.stock_qty = flt(ctx.stock_qty) or 1
out.stock_qty = flt(args.stock_qty) or 1
out.stock_uom = item.stock_uom
out.warehouse = get_item_warehouse_(ctx, item, overwrite_warehouse=True) if item else None
out.warehouse = get_item_warehouse(item, args, overwrite_warehouse=True) if item else None
# Cost Center
item_defaults = get_item_defaults(item.name, ctx.company)
item_group_defaults = get_item_group_defaults(item.name, ctx.company)
brand_defaults = get_brand_defaults(item.name, ctx.company)
out.cost_center = get_default_cost_center(ctx, item_defaults, item_group_defaults, brand_defaults)
item_defaults = get_item_defaults(item.name, args.company)
item_group_defaults = get_item_group_defaults(item.name, args.company)
brand_defaults = get_brand_defaults(item.name, args.company)
out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults)
if ctx.item_code and out.warehouse:
if args.item_code and out.warehouse:
incoming_rate_args = frappe._dict(
{
"item_code": ctx.item_code,
"item_code": args.item_code,
"warehouse": out.warehouse,
"posting_date": ctx.posting_date,
"posting_time": ctx.posting_time,
"posting_date": args.posting_date,
"posting_time": args.posting_time,
"qty": -1 * flt(out.stock_qty),
"voucher_type": ctx.doctype,
"voucher_no": ctx.name,
"company": ctx.company,
"serial_no": ctx.serial_no,
"batch_no": ctx.batch_no,
"voucher_type": args.doctype,
"voucher_no": args.name,
"company": args.company,
"serial_no": args.serial_no,
"batch_no": args.batch_no,
}
)
out.update(get_warehouse_details(incoming_rate_args))
@@ -849,28 +851,31 @@ def get_warehouse_details(args):
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_consumed_asset_details(ctx):
def get_consumed_asset_details(args):
if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
out = frappe._dict()
asset_details = frappe._dict()
if ctx.asset:
if args.asset:
asset_details = frappe.db.get_value(
"Asset", ctx.asset, ["asset_name", "item_code", "item_name"], as_dict=1
"Asset", args.asset, ["asset_name", "item_code", "item_name"], as_dict=1
)
if not asset_details:
frappe.throw(_("Asset {0} does not exist").format(ctx.asset))
frappe.throw(_("Asset {0} does not exist").format(args.asset))
out.item_code = asset_details.item_code
out.asset_name = asset_details.asset_name
out.item_name = asset_details.item_name
if ctx.asset:
if args.asset:
out.current_asset_value = flt(
get_asset_value_after_depreciation(ctx.asset, finance_book=ctx.finance_book)
get_asset_value_after_depreciation(args.asset, finance_book=args.finance_book)
)
out.asset_value = get_value_after_depreciation_on_disposal_date(
ctx.asset, ctx.posting_date, finance_book=ctx.finance_book
args.asset, args.posting_date, finance_book=args.finance_book
)
else:
out.current_asset_value = 0
@@ -879,7 +884,7 @@ def get_consumed_asset_details(ctx):
# Account
if asset_details.item_code:
out.fixed_asset_account = get_asset_category_account(
"fixed_asset_account", item=asset_details.item_code, company=ctx.company
"fixed_asset_account", item=asset_details.item_code, company=args.company
)
else:
out.fixed_asset_account = None
@@ -887,32 +892,37 @@ def get_consumed_asset_details(ctx):
# Cost Center
if asset_details.item_code:
item = frappe.get_cached_doc("Item", asset_details.item_code)
item_defaults = get_item_defaults(item.name, ctx.company)
item_group_defaults = get_item_group_defaults(item.name, ctx.company)
brand_defaults = get_brand_defaults(item.name, ctx.company)
out.cost_center = get_default_cost_center(ctx, item_defaults, item_group_defaults, brand_defaults)
item_defaults = get_item_defaults(item.name, args.company)
item_group_defaults = get_item_group_defaults(item.name, args.company)
brand_defaults = get_brand_defaults(item.name, args.company)
out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults)
return out
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_service_item_details(ctx):
def get_service_item_details(args):
if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
out = frappe._dict()
item = frappe._dict()
if ctx.item_code:
item = frappe.get_cached_doc("Item", ctx.item_code)
if args.item_code:
item = frappe.get_cached_doc("Item", args.item_code)
out.item_name = item.item_name
out.qty = flt(ctx.qty) or 1
out.qty = flt(args.qty) or 1
out.uom = item.purchase_uom or item.stock_uom
item_defaults = get_item_defaults(item.name, ctx.company)
item_group_defaults = get_item_group_defaults(item.name, ctx.company)
brand_defaults = get_brand_defaults(item.name, ctx.company)
item_defaults = get_item_defaults(item.name, args.company)
item_group_defaults = get_item_group_defaults(item.name, args.company)
brand_defaults = get_brand_defaults(item.name, args.company)
out.expense_account = get_default_expense_account(ctx, item_defaults, item_group_defaults, brand_defaults)
out.cost_center = get_default_cost_center(ctx, item_defaults, item_group_defaults, brand_defaults)
out.expense_account = get_default_expense_account(
args, item_defaults, item_group_defaults, brand_defaults
)
out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults)
return out

View File

@@ -4,7 +4,6 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Asset",
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[]",
"function": "Sum",
"idx": 0,

View File

@@ -3,7 +3,6 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Asset",
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Asset\",\"creation\",\"Timespan\",\"this year\",false]]",
"function": "Count",
"idx": 0,

View File

@@ -3,7 +3,6 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Asset",
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[]",
"function": "Count",
"idx": 0,

View File

@@ -93,10 +93,7 @@ frappe.ui.form.on("Purchase Order", {
get_materials_from_supplier: function (frm) {
let po_details = [];
if (
frm.doc.supplied_items &&
(flt(frm.doc.per_received, precision("per_received")) == 100 || frm.doc.status === "Closed")
) {
if (frm.doc.supplied_items && (flt(frm.doc.per_received, 2) == 100 || frm.doc.status === "Closed")) {
frm.doc.supplied_items.forEach((d) => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name);
@@ -332,8 +329,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
if (!["Closed", "Delivered"].includes(doc.status)) {
if (
this.frm.doc.status !== "Closed" &&
flt(this.frm.doc.per_received, precision("per_received")) < 100 &&
flt(this.frm.doc.per_billed, precision("per_billed")) < 100
flt(this.frm.doc.per_received, 2) < 100 &&
flt(this.frm.doc.per_billed, 2) < 100
) {
if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) {
this.frm.add_custom_button(__("Update Items"), () => {
@@ -347,10 +344,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
}
if (this.frm.has_perm("submit")) {
if (
flt(doc.per_billed, precision("per_billed")) < 100 ||
flt(doc.per_received, precision("per_received")) < 100
) {
if (flt(doc.per_billed, 2) < 100 || flt(doc.per_received, 2) < 100) {
if (doc.status != "On Hold") {
this.frm.add_custom_button(
__("Hold"),
@@ -388,7 +382,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
if (doc.status != "Closed") {
if (doc.status != "On Hold") {
if (flt(doc.per_received, precision("per_received")) < 100 && allow_receipt) {
if (flt(doc.per_received) < 100 && allow_receipt) {
this.frm.add_custom_button(
__("Purchase Receipt"),
() => {
@@ -419,7 +413,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
}
// Please do not add precision in the below flt function
if (flt(doc.per_billed, precision("per_billed")) < 100)
if (flt(doc.per_billed) < 100)
this.frm.add_custom_button(
__("Purchase Invoice"),
() => {
@@ -428,7 +422,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
__("Create")
);
if (flt(doc.per_billed, precision("per_billed")) < 100 && doc.status != "Delivered") {
if (flt(doc.per_billed, 2) < 100 && doc.status != "Delivered") {
this.frm.add_custom_button(
__("Payment"),
() => this.make_payment_entry(),
@@ -436,7 +430,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
if (flt(doc.per_billed, precision("per_billed")) < 100) {
if (flt(doc.per_billed, 2) < 100) {
this.frm.add_custom_button(
__("Payment Request"),
function () {

View File

@@ -18,7 +18,6 @@ def execute(filters=None):
columns = get_columns(filters)
data = get_data(filters)
update_received_amount(data)
if not data:
return [], [], None, []
@@ -41,6 +40,7 @@ def get_data(filters):
po = frappe.qb.DocType("Purchase Order")
po_item = frappe.qb.DocType("Purchase Order Item")
pi_item = frappe.qb.DocType("Purchase Invoice Item")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(po)
@@ -48,6 +48,8 @@ def get_data(filters):
.on(po_item.parent == po.name)
.left_join(pi_item)
.on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1))
.left_join(pr_item)
.on((pr_item.purchase_order_item == po_item.name) & (pr_item.docstatus == 1))
.select(
po.transaction_date.as_("date"),
po_item.schedule_date.as_("required_date"),
@@ -61,6 +63,7 @@ def get_data(filters):
(po_item.qty - po_item.received_qty).as_("pending_qty"),
Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"),
po_item.base_amount.as_("amount"),
(pr_item.base_amount).as_("received_qty_amount"),
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),
(po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_(
"pending_amount"
@@ -92,39 +95,6 @@ def get_data(filters):
return data
def update_received_amount(data):
pr_data = get_received_amount_data(data)
for row in data:
row.received_qty_amount = flt(pr_data.get(row.name))
def get_received_amount_data(data):
pr = frappe.qb.DocType("Purchase Receipt")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(pr)
.inner_join(pr_item)
.on(pr_item.parent == pr.name)
.select(
pr_item.purchase_order_item,
Sum(pr_item.base_amount).as_("received_qty_amount"),
)
.where((pr_item.parent == pr.name) & (pr.docstatus == 1))
.groupby(pr_item.purchase_order_item)
)
query = query.where(pr_item.purchase_order_item.isin([row.name for row in data]))
data = query.run()
if not data:
return frappe._dict()
return frappe._dict(data)
def prepare_data(data, filters):
completed, pending = 0, 0
pending_field = "pending_amount"

View File

@@ -62,12 +62,11 @@ from erpnext.setup.utils import get_exchange_rate
from erpnext.stock.doctype.item.item import get_uom_conv_factor
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.get_item_details import (
ItemDetailsCtx,
_get_item_tax_template,
get_conversion_factor,
get_item_details,
get_item_tax_map,
get_item_warehouse_,
get_item_warehouse,
)
from erpnext.utilities.regional import temporary_flag
from erpnext.utilities.transaction_base import TransactionBase
@@ -753,28 +752,24 @@ class AccountsController(TransactionBase):
for item in self.get("items"):
if item.get("item_code"):
ctx: ItemDetailsCtx = ItemDetailsCtx(parent_dict.copy())
ctx.update(item.as_dict())
args = parent_dict.copy()
args.update(item.as_dict())
ctx.update(
{
"doctype": self.doctype,
"name": self.name,
"child_doctype": item.doctype,
"child_docname": item.name,
"ignore_pricing_rule": (
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
),
}
args["doctype"] = self.doctype
args["name"] = self.name
args["child_doctype"] = item.doctype
args["child_docname"] = item.name
args["ignore_pricing_rule"] = (
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
)
if not ctx.transaction_date:
ctx.transaction_date = ctx.posting_date
if not args.get("transaction_date"):
args["transaction_date"] = args.get("posting_date")
if self.get("is_subcontracted"):
ctx.is_subcontracted = self.is_subcontracted
args["is_subcontracted"] = self.is_subcontracted
ret = get_item_details(ctx, self, for_validate=for_validate, overwrite_warehouse=False)
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
if item.get(fieldname) is None or fieldname in force_item_fields:
@@ -961,7 +956,6 @@ class AccountsController(TransactionBase):
"account_head": account_head,
"rate": 0,
"description": account_head,
"set_by_item_tax_template": 1,
},
)
@@ -3167,21 +3161,18 @@ def get_supplier_block_status(party_name):
def set_child_tax_template_and_map(item, child_item, parent_doc):
ctx = ItemDetailsCtx(
{
"item_code": item.item_code,
"posting_date": parent_doc.transaction_date,
"tax_category": parent_doc.get("tax_category"),
"company": parent_doc.get("company"),
}
)
args = {
"item_code": item.item_code,
"posting_date": parent_doc.transaction_date,
"tax_category": parent_doc.get("tax_category"),
"company": parent_doc.get("company"),
}
child_item.item_tax_template = _get_item_tax_template(ctx, item.taxes)
child_item.item_tax_rate = get_item_tax_map(
doc=parent_doc,
tax_template=child_item.item_tax_template,
as_json=True,
)
child_item.item_tax_template = _get_item_tax_template(args, item.taxes)
if child_item.get("item_tax_template"):
child_item.item_tax_rate = get_item_tax_map(
parent_doc.get("company"), child_item.item_tax_template, as_json=True
)
def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
@@ -3204,7 +3195,6 @@ def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
"charge_type": "On Net Total",
"account_head": tax_type,
"rate": tax_rate,
"set_by_item_tax_template": 1,
}
)
if parent_doc.doctype == "Purchase Order":
@@ -3228,7 +3218,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
child_item.stock_uom = item.stock_uom
child_item.uom = trans_item.get("uom") or item.stock_uom
child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True)
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
@@ -3237,7 +3227,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
child_item.base_rate = 1
child_item.base_amount = 1
if child_doctype == "Sales Order Item":
child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True)
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse:
frappe.throw(
_(

View File

@@ -14,7 +14,7 @@ from frappe.utils import nowdate, today, unique
from pypika import Order
import erpnext
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template
from erpnext.stock.get_item_details import _get_item_tax_template
# searches for active employees
@@ -813,16 +813,14 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
valid_from = filters.get("valid_from")
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
ctx = ItemDetailsCtx(
{
"item_code": filters.get("item_code"),
"posting_date": valid_from,
"tax_category": filters.get("tax_category"),
"company": company,
}
)
args = {
"item_code": filters.get("item_code"),
"posting_date": valid_from,
"tax_category": filters.get("tax_category"),
"company": company,
}
taxes = _get_item_tax_template(ctx, taxes, for_validate=True)
taxes = _get_item_tax_template(args, taxes, for_validate=True)
return [(d,) for d in set(taxes)]

View File

@@ -21,15 +21,9 @@ class SellingController(StockController):
def onload(self):
super().onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice", "Quotation"):
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
for item in self.get("items") + (self.get("packed_items") or []):
company = self.company
item.update(
get_bin_details(
item.item_code, item.warehouse, company=company, include_child_warehouses=True
)
)
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
def validate(self):
super().validate()
@@ -173,9 +167,6 @@ class SellingController(StockController):
total = 0.0
sales_team = self.get("sales_team")
self.validate_sales_team(sales_team)
for sales_person in sales_team:
self.round_floats_in(sales_person)
@@ -195,20 +186,6 @@ class SellingController(StockController):
if sales_team and total != 100.0:
throw(_("Total allocated percentage for sales team should be 100"))
def validate_sales_team(self, sales_team):
sales_persons = [d.sales_person for d in sales_team]
if not sales_persons:
return
sales_person_status = frappe.db.get_all(
"Sales Person", filters={"name": ["in", sales_persons]}, fields=["name", "enabled"]
)
for row in sales_person_status:
if not row.enabled:
frappe.throw(_("Sales Person <b>{0}</b> is disabled.").format(row.name))
def validate_max_discount(self):
for d in self.get("items"):
if d.item_code:

View File

@@ -822,9 +822,6 @@ class StockController(AccountsController):
self.update_inventory_dimensions(d, sl_dict)
if self.docstatus == 2:
from erpnext.deprecation_dumpster import deprecation_warning
deprecation_warning("unknown", "v16", "No instructions.")
# To handle denormalized serial no records, will br deprecated in v16
for field in ["serial_no", "batch_no"]:
if d.get(field):
@@ -1011,13 +1008,11 @@ class StockController(AccountsController):
def validate_qi_presence(self, row):
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
if not row.quality_inspection:
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
row.idx, frappe.bold(row.item_code)
)
msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
if self.docstatus == 1:
frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError)
frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
else:
frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue")
frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
def validate_qi_submission(self, row):
"""Check if QI is submitted on row level, during submission"""
@@ -1026,13 +1021,11 @@ class StockController(AccountsController):
if qa_docstatus != 1:
link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
msg = _("Row #{0}: Quality Inspection {1} is not submitted for the item: {2}").format(
row.idx, link, row.item_code
)
msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
if action == "Stop":
frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
else:
frappe.msgprint(msg, alert=True, indicator="orange")
frappe.msgprint(_(msg), alert=True, indicator="orange")
def validate_qi_rejection(self, row):
"""Check if QI is rejected on row level, during submission"""
@@ -1041,13 +1034,11 @@ class StockController(AccountsController):
if qa_status == "Rejected":
link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
msg = _("Row #{0}: Quality Inspection {1} was rejected for item {2}").format(
row.idx, link, row.item_code
)
msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
if action == "Stop":
frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
else:
frappe.msgprint(msg, alert=True, indicator="orange")
frappe.msgprint(_(msg), alert=True, indicator="orange")
def update_blanket_order(self):
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))

View File

@@ -335,18 +335,12 @@ class SubcontractingController(StockController):
# Will be deprecated in v16
if row.serial_no and not consumed_bundles.serial_nos:
from erpnext.deprecation_dumpster import deprecation_warning
deprecation_warning("unknown", "v16", "No instructions.")
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
)
# Will be deprecated in v16
if row.batch_no and not consumed_bundles.batch_nos:
from erpnext.deprecation_dumpster import deprecation_warning
deprecation_warning("unknown", "v16", "No instructions.")
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
def get_available_materials(self):

View File

@@ -8,6 +8,7 @@ import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
from frappe.utils.deprecations import deprecated
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
@@ -17,12 +18,9 @@ from erpnext.controllers.accounts_controller import (
validate_inclusive_tax,
validate_taxes_and_charges,
)
from erpnext.deprecation_dumpster import deprecated
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template
from erpnext.stock.get_item_details import _get_item_tax_template
from erpnext.utilities.regional import temporary_flag
logger = frappe.logger(__name__)
ItemWiseTaxDetail = frappe._dict
@@ -103,17 +101,15 @@ class calculate_taxes_and_totals:
for item in self.doc.items:
if item.item_code and item.get("item_tax_template"):
item_doc = frappe.get_cached_doc("Item", item.item_code)
ctx = ItemDetailsCtx(
{
"net_rate": item.net_rate or item.rate,
"base_net_rate": item.base_net_rate or item.base_rate,
"tax_category": self.doc.get("tax_category"),
"posting_date": self.doc.get("posting_date"),
"bill_date": self.doc.get("bill_date"),
"transaction_date": self.doc.get("transaction_date"),
"company": self.doc.get("company"),
}
)
args = {
"net_rate": item.net_rate or item.rate,
"base_net_rate": item.base_net_rate or item.base_rate,
"tax_category": self.doc.get("tax_category"),
"posting_date": self.doc.get("posting_date"),
"bill_date": self.doc.get("bill_date"),
"transaction_date": self.doc.get("transaction_date"),
"company": self.doc.get("company"),
}
item_group = item_doc.item_group
item_group_taxes = []
@@ -129,7 +125,7 @@ class calculate_taxes_and_totals:
# No validation if no taxes in item or item group
continue
taxes = _get_item_tax_template(ctx, item_taxes + item_group_taxes, for_validate=True)
taxes = _get_item_tax_template(args, item_taxes + item_group_taxes, for_validate=True)
if taxes:
if item.item_tax_template not in taxes:
@@ -383,10 +379,8 @@ class calculate_taxes_and_totals:
]
)
logger.debug(f"{self.doc} ...")
for n, item in enumerate(self._items):
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
logger.debug(f" Item {n}: {item.item_code}" + (f" - {item_tax_map}" if item_tax_map else ""))
for i, tax in enumerate(self.doc.get("taxes")):
# tax_amount represents the amount of tax for the current step
current_net_amount, current_tax_amount = self.get_current_tax_and_net_amount(
@@ -453,9 +447,6 @@ class calculate_taxes_and_totals:
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment"),
)
logger.debug(
f" net_amount: {current_net_amount:<20} tax_amount: {current_tax_amount:<20} - {tax.description}"
)
def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax):
# if just for valuation, do not add the tax amount in total
@@ -486,24 +477,25 @@ class calculate_taxes_and_totals:
current_net_amount = 0.0
if tax.charge_type == "Actual":
current_net_amount = item.net_amount
# distribute the tax amount proportionally to each item row
actual = flt(tax.tax_amount, tax.precision("tax_amount"))
if tax.get("is_tax_withholding_account") and item.meta.get_field("apply_tds"):
if not item.get("apply_tds") or not self.doc.tax_withholding_net_total:
current_tax_amount = 0.0
current_net_amount = 0.0
else:
current_tax_amount = item.net_amount * actual / self.doc.tax_withholding_net_total
current_net_amount = item.net_amount
current_tax_amount = current_net_amount * actual / self.doc.tax_withholding_net_total
else:
current_net_amount = item.net_amount
current_tax_amount = (
item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
current_net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
)
elif tax.charge_type == "On Net Total":
if tax.account_head in item_tax_map:
current_net_amount = item.net_amount
current_tax_amount = (tax_rate / 100.0) * item.net_amount
current_net_amount = item.net_amount
current_tax_amount = (tax_rate / 100.0) * current_net_amount
elif tax.charge_type == "On Previous Row Amount":
current_net_amount = self.doc.get("taxes")[cint(tax.row_id) - 1].tax_amount_for_current_item
current_tax_amount = (tax_rate / 100.0) * current_net_amount
@@ -567,12 +559,7 @@ class calculate_taxes_and_totals:
tax.base_tax_amount = round(tax.base_tax_amount, 0)
tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_amount, 0)
@deprecated(
f"{__name__}.calculate_taxes_and_totals.manipulate_grand_total_for_inclusive_tax",
"unknown",
"v16",
"No known instructions.",
)
@deprecated
def manipulate_grand_total_for_inclusive_tax(self):
# for backward compatablility - if in case used by an external application
return self.adjust_grand_total_for_inclusive_tax()

View File

@@ -79,7 +79,7 @@ class TestTaxesAndTotals(FrappeTestCase):
"rate": 50,
},
)
self.doc.set_missing_item_details()
calculate_taxes_and_totals(self.doc)
expected_values = {

View File

@@ -3,7 +3,7 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Opportunity",
"dynamic_filters_json": "[[\"Opportunity\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"dynamic_filters_json": "[[\"Opportunity\",\"status\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Opportunity\",\"company\",\"=\",null,false]]",
"function": "Count",
"idx": 0,

View File

@@ -5,7 +5,7 @@ This file is the final resting place (or should we say, "retirement home"?) for
Each function or method that checks in here comes with its own personalized decorator, complete with:
1. The date it was marked for deprecation (its "over the hill" birthday)
2. The ERPNext version at the beginning of which it becomes an error and at the end of which it will be removed (its "graduation" to the great codebase in the sky)
2. The ERPNext version in which it will be removed (its "graduation" to the great codebase in the sky)
3. A user-facing note on alternative solutions (its "parting wisdom")
Warning: The global namespace herein is more patched up than a sailor's favorite pair of jeans. Proceed with caution and a sense of humor!
@@ -15,63 +15,52 @@ Remember, deprecated doesn't mean useless - it just means these functions are en
Enjoy your stay in the Deprecation Dumpster, where every function gets a second chance to shine (or at least, to not break everything).
"""
import functools
import re
import sys
import warnings
from frappe.deprecation_dumpster import Color, _deprecated, colorize
def colorize(text, color_code):
if sys.stdout.isatty():
return f"\033[{color_code}m{text}\033[0m"
return text
# we use Warning because DeprecationWarning has python default filters which would exclude them from showing
# see also frappe.__init__ enabling them when a dev_server
class ERPNextDeprecationError(Warning):
"""Deprecated feature in current version.
Raises an error by default but can be configured via PYTHONWARNINGS in an emergency.
"""
class Color:
RED = 91
YELLOW = 93
CYAN = 96
class ERPNextDeprecationWarning(Warning):
"""Deprecated feature in next version"""
...
class PendingERPNextDeprecationWarning(ERPNextDeprecationWarning):
"""Deprecated feature in develop beyond next version.
try:
# since python 3.13, PEP 702
from warnings import deprecated as _deprecated
except ImportError:
import functools
import warnings
from collections.abc import Callable
from typing import Optional, TypeVar, Union, overload
Warning ignored by default.
T = TypeVar("T", bound=Callable)
The deprecation decision may still be reverted or deferred at this stage.
Regardless, using the new variant is encouraged and stable.
"""
def _deprecated(message: str, category=ERPNextDeprecationWarning, stacklevel=1) -> Callable[[T], T]:
def decorator(func: T) -> T:
@functools.wraps(func)
def wrapper(*args, **kwargs):
if message:
warning_msg = f"{func.__name__} is deprecated.\n{message}"
else:
warning_msg = f"{func.__name__} is deprecated."
warnings.warn(warning_msg, category=category, stacklevel=stacklevel + 1)
return func(*args, **kwargs)
return wrapper
wrapper.__deprecated__ = True # hint for the type checker
warnings.simplefilter("error", ERPNextDeprecationError)
warnings.simplefilter("ignore", PendingERPNextDeprecationWarning)
class V15ERPNextDeprecationWarning(ERPNextDeprecationError):
pass
class V16ERPNextDeprecationWarning(ERPNextDeprecationWarning):
pass
class V17ERPNextDeprecationWarning(PendingERPNextDeprecationWarning):
pass
def __get_deprecation_class(graduation: str | None = None, class_name: str | None = None) -> type:
if graduation:
# Scrub the graduation string to ensure it's a valid class name
cleaned_graduation = re.sub(r"\W|^(?=\d)", "_", graduation.upper())
class_name = f"{cleaned_graduation}ERPNextDeprecationWarning"
current_module = sys.modules[__name__]
try:
return getattr(current_module, class_name)
except AttributeError:
return PendingDeprecationWarning
return decorator
def deprecated(original: str, marked: str, graduation: str, msg: str, stacklevel: int = 1):
@@ -90,7 +79,6 @@ def deprecated(original: str, marked: str, graduation: str, msg: str, stacklevel
wrapper = _deprecated(
colorize(f"It was marked on {marked} for removal from {graduation} with note: ", Color.RED)
+ colorize(f"{msg}", Color.YELLOW),
category=__get_deprecation_class(graduation),
stacklevel=stacklevel,
)
@@ -115,7 +103,7 @@ def deprecation_warning(marked: str, graduation: str, msg: str):
Color.RED,
)
+ colorize(f"{msg}\n", Color.YELLOW),
category=__get_deprecation_class(graduation),
category=ERPNextDeprecationWarning,
stacklevel=2,
)
@@ -137,27 +125,3 @@ def taxes_and_totals_get_itemised_taxable_amount(items):
itemised_taxable_amount[item_code] += item.net_amount
return itemised_taxable_amount
@deprecated(
"erpnext.stock.get_pos_profile_item_details",
"2024-11-19",
"v16",
"Use erpnext.stock.get_pos_profile_item_details_ with a flipped signature",
)
def get_pos_profile_item_details(company, ctx, pos_profile=None, update_data=False):
from erpnext.stock.get_item_details import get_pos_profile_item_details_
return get_pos_profile_item_details_(ctx, company, pos_profile=pos_profile, update_data=update_data)
@deprecated(
"erpnext.stock.get_item_warehouse",
"2024-11-19",
"v16",
"Use erpnext.stock.get_item_warehouse_ with a flipped signature",
)
def get_item_warehouse(item, ctx, overwrite_warehouse, defaults=None):
from erpnext.stock.get_item_details import get_item_warehouse_
return get_item_warehouse_(ctx, item, overwrite_warehouse, defaults=defaults)

View File

@@ -393,7 +393,6 @@ doc_events = {
# function should expect the variable and doc as arguments
naming_series_variables = {
"FY": "erpnext.accounts.utils.parse_naming_series_variable",
"ABBR": "erpnext.accounts.utils.parse_naming_series_variable",
}
# On cancel event Payment Entry will be exempted and all linked submittable doctype will get cancelled.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,7 @@ frappe.ui.form.on("BOM", {
item: row.finished_good,
is_active: 1,
docstatus: 1,
track_semi_finished_goods: 0,
},
};
});

View File

@@ -16,7 +16,7 @@ from frappe.website.website_generator import WebsiteGenerator
import erpnext
from erpnext.setup.utils import get_exchange_rate
from erpnext.stock.doctype.item.item import get_item_details
from erpnext.stock.get_item_details import ItemDetailsCtx, get_conversion_factor, get_price_list_rate
from erpnext.stock.get_item_details import get_conversion_factor, get_price_list_rate
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -886,7 +886,7 @@ class BOM(WebsiteGenerator):
self.cur_exploded_items = {}
for d in self.get("items"):
if d.bom_no:
self.get_child_exploded_items(d.bom_no, d.stock_qty, d.operation)
self.get_child_exploded_items(d.bom_no, d.stock_qty)
elif d.item_code:
self.add_to_cur_exploded_items(
frappe._dict(
@@ -915,7 +915,7 @@ class BOM(WebsiteGenerator):
else:
self.cur_exploded_items[args.item_code] = args
def get_child_exploded_items(self, bom_no, stock_qty, operation=None):
def get_child_exploded_items(self, bom_no, stock_qty):
"""Add all items from Flat BOM of child BOM"""
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
child_fb_items = frappe.db.sql(
@@ -949,7 +949,7 @@ class BOM(WebsiteGenerator):
"item_code": d["item_code"],
"item_name": d["item_name"],
"source_warehouse": d["source_warehouse"],
"operation": d["operation"] or operation,
"operation": d["operation"],
"description": d["description"],
"stock_uom": d["stock_uom"],
"stock_qty": d["qty_consumed_per_unit"] * stock_qty,
@@ -1052,7 +1052,7 @@ def get_bom_item_rate(args, bom_doc):
elif bom_doc.rm_cost_as_per == "Price List":
if not bom_doc.buying_price_list:
frappe.throw(_("Please select Price List"))
ctx = ItemDetailsCtx(
bom_args = frappe._dict(
{
"doctype": "BOM",
"price_list": bom_doc.buying_price_list,
@@ -1070,7 +1070,7 @@ def get_bom_item_rate(args, bom_doc):
}
)
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
price_list_data = get_price_list_rate(ctx, item_doc)
price_list_data = get_price_list_rate(bom_args, item_doc)
rate = price_list_data.price_list_rate
return flt(rate)

View File

@@ -98,6 +98,8 @@ frappe.ui.form.on("BOM Creator", {
],
primary_action_label: __("Create"),
primary_action: (values) => {
frm.events.validate_dialog_values(frm, values);
values.doctype = frm.doc.doctype;
frappe.db.insert(values).then((doc) => {
frappe.set_route("Form", doc.doctype, doc.name);
@@ -109,6 +111,18 @@ frappe.ui.form.on("BOM Creator", {
dialog.show();
},
validate_dialog_values(frm, values) {
if (values.track_semi_finished_goods) {
if (values.final_operation_time <= 0) {
frappe.throw(__("Operation Time must be greater than 0"));
}
if (!values.workstation && !values.workstation_type) {
frappe.throw(__("Either Workstation or Workstation Type is mandatory"));
}
}
},
set_queries(frm) {
frm.set_query("bom_no", "items", function (doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);

View File

@@ -39,6 +39,23 @@
"items",
"costing_detail",
"raw_material_cost",
"configuration_section",
"track_operations",
"column_break_obzr",
"track_semi_finished_goods",
"final_product_operation_section",
"operation",
"operation_time",
"column_break_xnlu",
"workstation_type",
"workstation",
"final_product_warehouse_section",
"skip_material_transfer",
"backflush_from_wip_warehouse",
"source_warehouse",
"column_break_buha",
"wip_warehouse",
"fg_warehouse",
"remarks_tab",
"remarks",
"section_break_yixm",
@@ -281,6 +298,104 @@
"label": "Error Log",
"read_only": 1
},
{
"fieldname": "configuration_section",
"fieldtype": "Section Break",
"label": "Operation"
},
{
"default": "0",
"depends_on": "track_operations",
"fieldname": "track_semi_finished_goods",
"fieldtype": "Check",
"label": "Track Semi Finished Goods"
},
{
"fieldname": "column_break_obzr",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "track_operations",
"fieldtype": "Check",
"label": "Track Operations"
},
{
"depends_on": "eval:doc.track_semi_finished_goods === 1",
"fieldname": "final_product_operation_section",
"fieldtype": "Section Break",
"label": "Final Product Operation & Workstation"
},
{
"fieldname": "column_break_xnlu",
"fieldtype": "Column Break"
},
{
"fieldname": "operation",
"fieldtype": "Link",
"label": "Operation",
"options": "Operation"
},
{
"fieldname": "operation_time",
"fieldtype": "Float",
"label": "Operation Time (in mins)"
},
{
"fieldname": "workstation",
"fieldtype": "Link",
"label": "Workstation",
"options": "Workstation"
},
{
"fieldname": "workstation_type",
"fieldtype": "Link",
"label": "Workstation Type",
"options": "Workstation Type"
},
{
"depends_on": "eval:!doc.backflush_from_wip_warehouse",
"fieldname": "source_warehouse",
"fieldtype": "Link",
"label": "Source Warehouse",
"options": "Warehouse"
},
{
"depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
"fieldname": "wip_warehouse",
"fieldtype": "Link",
"label": "Work In Progress Warehouse",
"options": "Warehouse"
},
{
"fieldname": "fg_warehouse",
"fieldtype": "Link",
"label": "Finished Good Warehouse",
"options": "Warehouse"
},
{
"depends_on": "eval:doc.track_semi_finished_goods === 1",
"fieldname": "final_product_warehouse_section",
"fieldtype": "Section Break",
"label": "Final Product Warehouse"
},
{
"default": "0",
"fieldname": "skip_material_transfer",
"fieldtype": "Check",
"label": "Skip Material Transfer"
},
{
"default": "0",
"depends_on": "eval:doc.skip_material_transfer",
"fieldname": "backflush_from_wip_warehouse",
"fieldtype": "Check",
"label": "Backflush Materials From WIP Warehouse"
},
{
"fieldname": "column_break_buha",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_xvld",
"fieldtype": "Section Break",
@@ -302,7 +417,7 @@
"link_fieldname": "bom_creator"
}
],
"modified": "2024-11-25 16:41:03.047835",
"modified": "2024-09-26 17:07:32.111198",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator",

View File

@@ -44,16 +44,20 @@ class BOMCreator(Document):
from erpnext.manufacturing.doctype.bom_creator_item.bom_creator_item import BOMCreatorItem
amended_from: DF.Link | None
backflush_from_wip_warehouse: DF.Check
buying_price_list: DF.Link | None
company: DF.Link
conversion_rate: DF.Float
currency: DF.Link
default_warehouse: DF.Link | None
error_log: DF.Text | None
fg_warehouse: DF.Link | None
item_code: DF.Link
item_group: DF.Link | None
item_name: DF.Data | None
items: DF.Table[BOMCreatorItem]
operation: DF.Link | None
operation_time: DF.Float
plc_conversion_rate: DF.Float
price_list_currency: DF.Link | None
project: DF.Link | None
@@ -63,8 +67,15 @@ class BOMCreator(Document):
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
routing: DF.Link | None
set_rate_based_on_warehouse: DF.Check
skip_material_transfer: DF.Check
source_warehouse: DF.Link | None
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
track_operations: DF.Check
track_semi_finished_goods: DF.Check
uom: DF.Link | None
wip_warehouse: DF.Link | None
workstation: DF.Link | None
workstation_type: DF.Link | None
# end: auto-generated types
def before_save(self):
@@ -261,9 +272,15 @@ class BOMCreator(Document):
try:
for d in reverse_tree:
if self.track_operations and self.track_semi_finished_goods and final_product == d:
continue
fg_item_data = production_item_wise_rm.get(d).fg_item_data
self.create_bom(fg_item_data, production_item_wise_rm)
if self.track_operations and self.track_semi_finished_goods:
self.make_bom_for_final_product(production_item_wise_rm)
frappe.msgprint(_("BOMs created successfully"))
except Exception:
traceback = frappe.get_traceback(with_context=True)
@@ -276,6 +293,81 @@ class BOMCreator(Document):
frappe.msgprint(_("BOMs creation failed"))
def make_bom_for_final_product(self, production_item_wise_rm):
bom = frappe.new_doc("BOM")
bom.update(
{
"item": self.item_code,
"bom_type": "Production",
"quantity": self.qty,
"allow_alternative_item": 1,
"bom_creator": self.name,
"bom_creator_item": self.name,
"rm_cost_as_per": "Manual",
"with_operations": 1,
"track_semi_finished_goods": 1,
}
)
for field in BOM_FIELDS:
if self.get(field):
bom.set(field, self.get(field))
for item in self.items:
if not item.is_expandable or not item.operation:
continue
bom.append(
"operations",
{
"operation": item.operation,
"workstation": item.workstation,
"source_warehouse": item.source_warehouse,
"wip_warehouse": item.wip_warehouse,
"fg_warehouse": item.fg_warehouse,
"finished_good": item.item_code,
"finished_good_qty": item.qty,
"bom_no": production_item_wise_rm[(item.item_code, item.name)].bom_no,
"workstation_type": item.workstation_type,
"time_in_mins": item.operation_time,
"is_subcontracted": item.is_subcontracted,
"skip_material_transfer": item.skip_material_transfer,
"backflush_from_wip_warehouse": item.backflush_from_wip_warehouse,
},
)
operation_row = bom.append(
"operations",
{
"operation": self.operation,
"time_in_mins": self.operation_time,
"workstation": self.workstation,
"workstation_type": self.workstation_type,
"finished_good": self.item_code,
"finished_good_qty": self.qty,
"source_warehouse": self.source_warehouse,
"wip_warehouse": self.wip_warehouse,
"fg_warehouse": self.fg_warehouse,
"skip_material_transfer": self.skip_material_transfer,
"backflush_from_wip_warehouse": self.backflush_from_wip_warehouse,
},
)
final_product = (self.item_code, self.name)
items = production_item_wise_rm.get(final_product).get("items")
bom.set_materials_based_on_operation_bom()
for item in items:
item_args = {"operation_row_id": operation_row.idx}
for field in BOM_ITEM_FIELDS:
item_args[field] = item.get(field)
bom.append("items", item_args)
bom.save(ignore_permissions=True)
bom.submit()
def create_bom(self, row, production_item_wise_rm):
bom_creator_item = row.name if row.name != self.name else ""
if frappe.db.exists(
@@ -301,7 +393,25 @@ class BOMCreator(Document):
}
)
if row.item_code == self.item_code and (self.routing or self.has_operations()):
if self.track_operations and not self.track_semi_finished_goods:
if row.item_code == self.item_code:
bom.with_operations = 1
bom.transfer_material_against = "Work Order"
for item in self.items:
if not item.operation:
continue
bom.append(
"operations",
{
"operation": item.operation,
"workstation_type": item.workstation_type,
"workstation": item.workstation,
"time_in_mins": item.operation_time,
},
)
elif row.item_code == self.item_code and self.routing:
bom.routing = self.routing
bom.with_operations = 1
bom.transfer_material_against = "Work Order"
@@ -337,13 +447,6 @@ class BOMCreator(Document):
production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name
def has_operations(self):
for row in self.items:
if row.operation:
return True
return False
@frappe.whitelist()
def get_default_bom(self, item_code) -> str:
return frappe.get_cached_value("Item", item_code, "default_bom")
@@ -459,6 +562,14 @@ def add_sub_assembly(**kwargs):
"is_expandable": 1,
"stock_uom": item_info.stock_uom,
"operation": bom_item.operation,
"workstation_type": bom_item.workstation_type,
"operation_time": bom_item.operation_time,
"is_subcontracted": bom_item.is_subcontracted,
"workstation": bom_item.workstation,
"source_warehouse": bom_item.source_warehouse,
"wip_warehouse": bom_item.wip_warehouse,
"fg_warehouse": bom_item.fg_warehouse,
"skip_material_transfer": bom_item.skip_material_transfer,
},
)
@@ -468,6 +579,20 @@ def add_sub_assembly(**kwargs):
parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
if parent_row_no:
parent_row_no = parent_row_no[0]
doc.items[parent_row_no - 1].update(
{
"operation": bom_item.operation,
"workstation_type": bom_item.workstation_type,
"operation_time": bom_item.operation_time,
"is_subcontracted": bom_item.is_subcontracted,
"workstation": bom_item.workstation,
"source_warehouse": bom_item.source_warehouse,
"wip_warehouse": bom_item.wip_warehouse,
"fg_warehouse": bom_item.fg_warehouse,
"skip_material_transfer": bom_item.skip_material_transfer,
"backflush_from_wip_warehouse": bom_item.backflush_from_wip_warehouse,
}
)
for row in bom_item.get("items"):
row = frappe._dict(row)

View File

@@ -17,7 +17,17 @@
"is_subcontracted",
"operation_section",
"operation",
"operation_time",
"column_break_cbnk",
"workstation_type",
"workstation",
"warehouse_section",
"skip_material_transfer",
"backflush_from_wip_warehouse",
"source_warehouse",
"column_break_xutc",
"wip_warehouse",
"fg_warehouse",
"description_section",
"description",
"quantity_and_rate_section",
@@ -77,6 +87,13 @@
"options": "Item",
"reqd": 1
},
{
"depends_on": "eval:doc.skip_material_transfer && !doc.backflush_from_wip_warehouse",
"fieldname": "source_warehouse",
"fieldtype": "Link",
"label": "Source Warehouse",
"options": "Warehouse"
},
{
"columns": 1,
"default": "0",
@@ -239,6 +256,59 @@
"fieldname": "column_break_cbnk",
"fieldtype": "Column Break"
},
{
"fieldname": "workstation_type",
"fieldtype": "Link",
"label": "Workstation Type",
"options": "Workstation Type"
},
{
"description": "In Mins",
"fieldname": "operation_time",
"fieldtype": "Int",
"label": "Operation Time"
},
{
"fieldname": "workstation",
"fieldtype": "Link",
"label": "Workstation",
"options": "Workstation"
},
{
"fieldname": "warehouse_section",
"fieldtype": "Section Break",
"label": "Warehouse"
},
{
"depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
"fieldname": "wip_warehouse",
"fieldtype": "Link",
"label": "Work In Progress Warehouse",
"options": "Warehouse"
},
{
"fieldname": "column_break_xutc",
"fieldtype": "Column Break"
},
{
"fieldname": "fg_warehouse",
"fieldtype": "Link",
"label": "Finished Good Warehouse",
"options": "Warehouse"
},
{
"default": "0",
"fieldname": "skip_material_transfer",
"fieldtype": "Check",
"label": "Skip Material Transfer"
},
{
"default": "0",
"depends_on": "eval:doc.skip_material_transfer",
"fieldname": "backflush_from_wip_warehouse",
"fieldtype": "Check",
"label": "Backflush Materials From WIP Warehouse"
},
{
"default": "0",
"fieldname": "is_subcontracted",
@@ -250,7 +320,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-11-25 18:13:34.542391",
"modified": "2024-06-03 18:45:24.339532",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator Item",

View File

@@ -15,6 +15,7 @@ class BOMCreatorItem(Document):
from frappe.types import DF
amount: DF.Currency
backflush_from_wip_warehouse: DF.Check
base_amount: DF.Currency
base_rate: DF.Currency
bom_created: DF.Check
@@ -23,6 +24,7 @@ class BOMCreatorItem(Document):
do_not_explode: DF.Check
fg_item: DF.Link
fg_reference_id: DF.Data | None
fg_warehouse: DF.Link | None
instruction: DF.SmallText | None
is_expandable: DF.Check
is_subcontracted: DF.Check
@@ -30,16 +32,22 @@ class BOMCreatorItem(Document):
item_group: DF.Link | None
item_name: DF.Data | None
operation: DF.Link | None
operation_time: DF.Int
parent: DF.Data
parent_row_no: DF.Data | None
parentfield: DF.Data
parenttype: DF.Data
qty: DF.Float
rate: DF.Currency
skip_material_transfer: DF.Check
source_warehouse: DF.Link | None
sourced_by_supplier: DF.Check
stock_qty: DF.Float
stock_uom: DF.Link | None
uom: DF.Link | None
wip_warehouse: DF.Link | None
workstation: DF.Link | None
workstation_type: DF.Link | None
# end: auto-generated types
pass

View File

@@ -1157,9 +1157,6 @@ class JobCard(Document):
for employee in kwargs.employees:
kwargs.employee = employee.get("employee")
if kwargs.from_time and not kwargs.to_time:
if kwargs.qty:
kwargs.completed_qty = kwargs.qty
row = self.append("time_logs", kwargs)
row.db_update()
self.db_set("status", "Work In Progress")
@@ -1226,10 +1223,6 @@ class JobCard(Document):
if kwargs.auto_submit:
self.submit()
if not self.finished_good:
return
self.make_stock_entry_for_semi_fg_item(kwargs.auto_submit)
frappe.msgprint(
_("Job Card {0} has been completed").format(get_link_to_form("Job Card", self.name))

View File

@@ -154,7 +154,7 @@
{
"fieldname": "raw_materials_consumption_section",
"fieldtype": "Section Break",
"label": "Raw Materials Consumption"
"label": "Raw Materials Consumption "
},
{
"fieldname": "column_break_16",
@@ -267,4 +267,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -481,6 +481,7 @@ class ProductionPlan(Document):
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
)
<<<<<<< HEAD
bom_no = data.bom_no or item_details and item_details.get("bom_no") or ""
if not bom_no:
continue
@@ -499,6 +500,19 @@ class ProductionPlan(Document):
"product_bundle_item": data.parent_item,
},
)
=======
pi = self.append('po_items', {
'warehouse': data.warehouse,
'item_code': data.item_code,
'description': data.description or item_details.description,
'stock_uom': item_details and item_details.stock_uom or '',
'bom_no': item_details and item_details.bom_no or '',
'planned_qty': data.pending_qty,
'pending_qty': data.pending_qty,
'planned_start_date': now_datetime(),
'product_bundle_item': data.parent_item
})
>>>>>>> 69d88a9212 (fix: Production Plan: load document defaults for plan items & remove name column from listview (#26584))
pi._set_defaults()
if self.get_items_from == "Sales Order":

View File

@@ -1,4 +1,8 @@
<<<<<<< HEAD
frappe.listview_settings["Production Plan"] = {
=======
frappe.listview_settings['Production Plan'] = {
>>>>>>> 69d88a9212 (fix: Production Plan: load document defaults for plan items & remove name column from listview (#26584))
hide_name_column: true,
add_fields: ["status"],
filters: [["status", "!=", "Closed"]],

View File

@@ -53,24 +53,3 @@ class Routing(Document):
)
sequence_id = row.sequence_id
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_operations(doctype, txt, searchfield, start, page_len, filters):
query_filters = {}
if txt:
query_filters = {"operation": ["like", f"%{txt}%"]}
if filters.get("routing"):
query_filters["parent"] = filters.get("routing")
return frappe.get_all(
"BOM Operation",
fields=["operation"],
filters=query_filters,
start=start,
page_length=page_len,
as_list=1,
)

View File

@@ -1645,7 +1645,6 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
"sequence_id": row.get("sequence_id"),
"hour_rate": row.get("hour_rate"),
"serial_no": row.get("serial_no"),
"time_required": row.get("time_in_mins"),
"source_warehouse": row.get("source_warehouse"),
"target_warehouse": row.get("fg_warehouse"),
"wip_warehouse": work_order.wip_warehouse or row.get("wip_warehouse"),

View File

@@ -16,7 +16,6 @@
"include_item_in_manufacturing",
"qty_section",
"required_qty",
"stock_uom",
"rate",
"amount",
"column_break_11",
@@ -146,19 +145,11 @@
"fieldtype": "Int",
"label": "Operation Row Id",
"read_only": 1
},
{
"fetch_from": "item_code.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2024-11-19 15:48:16.823384",
"modified": "2024-03-27 13:12:00.429838",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Item",

View File

@@ -25,7 +25,6 @@ class WorkOrderItem(Document):
item_code: DF.Link | None
item_name: DF.Data | None
operation: DF.Link | None
operation_row_id: DF.Int
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
@@ -33,7 +32,6 @@ class WorkOrderItem(Document):
required_qty: DF.Float
returned_qty: DF.Float
source_warehouse: DF.Link | None
stock_uom: DF.Link | None
transferred_qty: DF.Float
# end: auto-generated types

View File

@@ -509,7 +509,6 @@ def update_job_card(job_card, method, **kwargs):
if kwargs.qty and isinstance(kwargs.qty, str):
kwargs.qty = flt(kwargs.qty)
print(method)
doc = frappe.get_doc("Job Card", job_card)
doc.run_method(method, **kwargs)

View File

@@ -3,7 +3,6 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Work Order",
"dynamic_filters_json": "[[\"Work Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Work Order\",\"status\",\"=\",\"Completed\"],[\"Work Order\",\"docstatus\",\"=\",1],[\"Work Order\",\"creation\",\"between\",[\"2020-06-08\",\"2020-07-08\"]]]",
"function": "Count",
"idx": 0,

View File

@@ -3,7 +3,6 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Work Order",
"dynamic_filters_json": "[[\"Work Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Work Order\",\"docstatus\",\"=\",1],[\"Work Order\",\"creation\",\"between\",[\"2020-06-08\",\"2020-07-08\"]]]",
"function": "Count",
"idx": 0,

View File

@@ -3,7 +3,6 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Job Card",
"dynamic_filters_json": "[[\"Job Card\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Job Card\",\"status\",\"!=\",\"Completed\"],[\"Job Card\",\"docstatus\",\"=\",1]]",
"function": "Count",
"idx": 0,

View File

@@ -389,4 +389,3 @@ erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries
erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_format
erpnext.patches.v14_0.update_stock_uom_in_work_order_item

View File

@@ -1,15 +0,0 @@
import frappe
def execute():
frappe.db.sql(
"""
UPDATE
`tabWork Order Item`, `tabItem`
SET
`tabWork Order Item`.stock_uom = `tabItem`.stock_uom
WHERE
`tabWork Order Item`.item_code = `tabItem`.name
AND `tabWork Order Item`.docstatus = 1
"""
)

View File

@@ -169,14 +169,10 @@ class Timesheet(Document):
task.save()
tasks.append(data.task)
if data.project and data.project not in projects:
elif data.project and data.project not in projects:
frappe.get_doc("Project", data.project).update_project()
projects.append(data.project)
for project in projects:
project_doc = frappe.get_doc("Project", project)
project_doc.update_project()
project_doc.save()
def validate_dates(self):
for time_log in self.time_logs:
time_log.validate_dates()

View File

@@ -292,7 +292,7 @@ class BOMConfigurator {
});
}
get_sub_assembly_modal_fields(view, is_root = false, read_only = false) {
get_sub_assembly_modal_fields(view, is_root = false, read_only = false, show_operations_fields = false) {
let fields = [
{
label: __("Sub Assembly Item"),
@@ -320,7 +320,7 @@ class BOMConfigurator {
},
];
if (is_root) {
if (this.frm.doc.track_operations && (is_root || show_operations_fields)) {
fields.push(
...[
{ fieldtype: "Section Break" },
@@ -329,18 +329,105 @@ class BOMConfigurator {
fieldname: "operation",
fieldtype: "Link",
options: "Operation",
get_query() {
let doc = view.events.frm.doc;
reqd: 1,
},
{
label: __("Operation Time"),
fieldname: "operation_time",
fieldtype: "Int",
reqd: 1,
},
{
label: __("Is Subcontracted"),
fieldname: "is_subcontracted",
fieldtype: "Check",
},
{ fieldtype: "Column Break" },
{
label: __("Workstation Type"),
fieldname: "workstation_type",
fieldtype: "Link",
options: "Workstation Type",
},
{
label: __("Workstation"),
fieldname: "workstation",
fieldtype: "Link",
options: "Workstation",
},
]
);
if (doc.routing) {
if (this.frm.doc.track_semi_finished_goods) {
fields.push(
...[
{ label: __("Default Warehouse"), fieldtype: "Section Break", collapsible: 1 },
{
label: __("Skip Material Transfer"),
fieldname: "skip_material_transfer",
fieldtype: "Check",
},
{
label: __("Backflush Materials From WIP"),
fieldname: "backflush_from_wip_warehouse",
fieldtype: "Check",
depends_on: "eval:doc.skip_material_transfer",
},
{
label: __("Source Warehouse"),
fieldname: "source_warehouse",
fieldtype: "Link",
options: "Warehouse",
depends_on: "eval:!doc.backflush_from_wip_warehouse",
get_query() {
return {
query: "erpnext.manufacturing.doctype.routing.routing.get_operations",
filters: {
routing: doc.routing,
company: view.events.frm.doc.company,
},
};
}
},
},
{ fieldtype: "Column Break" },
{
label: __("Work In Progress Warehouse"),
fieldname: "wip_warehouse",
fieldtype: "Link",
options: "Warehouse",
depends_on:
"eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
get_query() {
return {
filters: {
company: view.events.frm.doc.company,
},
};
},
},
{
label: __("Finished Good Warehouse"),
fieldname: "fg_warehouse",
fieldtype: "Link",
options: "Warehouse",
get_query() {
return {
filters: {
company: view.events.frm.doc.company,
},
};
},
},
]
);
}
} else if (this.frm.doc.routing && is_root) {
fields.push(
...[
{ fieldtype: "Section Break" },
{
label: __("Operation"),
fieldname: "operation",
fieldtype: "Link",
options: "Operation",
},
]
);
@@ -386,7 +473,7 @@ class BOMConfigurator {
convert_to_sub_assembly(node, view) {
let dialog = new frappe.ui.Dialog({
fields: view.events.get_sub_assembly_modal_fields(view, node.is_root, true),
fields: view.events.get_sub_assembly_modal_fields(view, node.is_root, true, true),
title: __("Add Sub Assembly"),
});
@@ -469,6 +556,123 @@ class BOMConfigurator {
let qty = node.data.qty || this.frm.doc.qty;
let fields = [{ label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }];
if (node.expandable && this.frm.doc.track_operations) {
let data = node.data.operation ? node.data : this.frm.doc;
fields = [
...fields,
...[
{ fieldtype: "Section Break" },
{
label: __("Operation"),
fieldname: "operation",
fieldtype: "Link",
options: "Operation",
default: data.operation,
},
{
label: __("Operation Time"),
fieldname: "operation_time",
fieldtype: "Float",
default: data.operation_time,
},
{
label: __("Is Subcontracted"),
fieldname: "is_subcontracted",
fieldtype: "Check",
hidden: node?.is_root || 0,
default: data.is_subcontracted,
},
{ fieldtype: "Column Break" },
{
label: __("Workstation Type"),
fieldname: "workstation_type",
fieldtype: "Link",
options: "Workstation Type",
default: data.workstation_type,
},
{
label: __("Workstation"),
fieldname: "workstation",
fieldtype: "Link",
options: "Workstation",
default: data.workstation,
get_query() {
let dialog = me.frm.edit_bom_dialog;
let workstation_type = dialog.get_value("workstation_type");
if (workstation_type) {
return {
filters: {
workstation_type: dialog.get_value("workstation_type"),
},
};
}
},
},
{ fieldtype: "Section Break" },
{
label: __("Skip Material Transfer"),
fieldname: "skip_material_transfer",
fieldtype: "Check",
default: data.skip_material_transfer,
},
{
label: __("Backflush Materials From WIP"),
fieldname: "backflush_from_wip_warehouse",
fieldtype: "Check",
depends_on: "eval:doc.skip_material_transfer",
default: data.backflush_from_wip_warehouse,
},
{
label: __("Source Warehouse"),
fieldname: "source_warehouse",
fieldtype: "Link",
options: "Warehouse",
default: data.source_warehouse,
depends_on: "eval:!doc.backflush_from_wip_warehouse",
get_query() {
return {
filters: {
company: me.frm.doc.company,
},
};
},
},
{ fieldtype: "Column Break" },
{
label: __("Work In Progress Warehouse"),
fieldname: "wip_warehouse",
fieldtype: "Link",
options: "Warehouse",
default: data.wip_warehouse,
depends_on: "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
get_query() {
return {
filters: {
company: me.frm.doc.company,
},
};
},
},
{
label: __("Finished Good Warehouse"),
fieldname: "fg_warehouse",
fieldtype: "Link",
options: "Warehouse",
default: data.fg_warehouse,
get_query() {
return {
filters: {
company: me.frm.doc.company,
},
};
},
},
],
];
}
this.frm.edit_bom_dialog = frappe.prompt(
fields,
(data) => {

View File

@@ -336,7 +336,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
child.charge_type = "On Net Total";
child.account_head = tax;
child.rate = 0;
child.set_by_item_tax_template = true;
}
});
}
@@ -448,6 +447,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
get_current_tax_amount(item, tax, item_tax_map) {
var tax_rate = this._get_tax_rate(tax, item_tax_map);
var current_tax_amount = 0.0;
var current_net_amount = 0.0;
// To set row_id by default as previous row.
if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
@@ -462,16 +462,20 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if(tax.charge_type == "Actual") {
// distribute the tax amount proportionally to each item row
var actual = flt(tax.tax_amount, precision("tax_amount", tax));
current_net_amount = item.net_amount
current_tax_amount = this.frm.doc.net_total ?
((item.net_amount / this.frm.doc.net_total) * actual) : 0.0;
} else if(tax.charge_type == "On Net Total") {
current_net_amount = item.net_amount
current_tax_amount = (tax_rate / 100.0) * item.net_amount;
} else if(tax.charge_type == "On Previous Row Amount") {
current_net_amount = this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount_for_current_item
current_tax_amount = (tax_rate / 100.0) *
this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_amount_for_current_item;
} else if(tax.charge_type == "On Previous Row Total") {
current_net_amount = this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item
current_tax_amount = (tax_rate / 100.0) *
this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item;
} else if (tax.charge_type == "On Item Quantity") {

View File

@@ -531,7 +531,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
child: item,
args: {
doc: me.frm.doc,
ctx: {
args: {
item_code: item.item_code,
barcode: item.barcode,
serial_no: item.serial_no,
@@ -751,7 +751,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
child.charge_type = "On Net Total";
child.account_head = tax;
child.rate = 0;
child.set_by_item_tax_template = true;
}
});
}
@@ -1382,7 +1381,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if(child.service_start_date) {
frappe.call({
"method": "erpnext.stock.get_item_details.calculate_service_end_date",
args: {ctx: child},
args: {"args": child},
callback: function(r) {
frappe.model.set_value(cdt, cdn, "service_end_date", r.message.service_end_date);
}
@@ -1549,7 +1548,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.call({
method: "erpnext.stock.get_item_details.get_batch_based_item_price",
args: {
pctx: params,
params: params,
item_code: row.item_code,
},
callback: function(r) {
@@ -1911,18 +1910,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
me.in_apply_price_list = true;
return this.frm.call({
method: "erpnext.stock.get_item_details.apply_price_list",
args: { ctx: args, doc: me.frm.doc },
args: { args: args, doc: me.frm.doc },
callback: function(r) {
if (!r.exc) {
frappe.run_serially([
() => {
if (r.message.parent.price_list_currency)
me.frm.set_value("price_list_currency", r.message.parent.price_list_currency);
},
() => {
if (r.message.parent.plc_conversion_rate)
me.frm.set_value("plc_conversion_rate", r.message.parent.plc_conversion_rate);
},
() => me.frm.set_value("price_list_currency", r.message.parent.price_list_currency),
() => me.frm.set_value("plc_conversion_rate", r.message.parent.plc_conversion_rate),
() => {
if(args.items.length) {
me._set_values_for_item_list(r.message.children);
@@ -2083,7 +2076,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return this.frm.call({
method: "erpnext.stock.get_item_details.get_item_tax_info",
args: {
doc: me.frm.doc,
company: me.frm.doc.company,
tax_category: cstr(me.frm.doc.tax_category),
item_codes: item_codes,
item_rates: item_rates,
@@ -2114,7 +2107,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return this.frm.call({
method: "erpnext.stock.get_item_details.get_item_tax_map",
args: {
doc: me.frm.doc,
company: me.frm.doc.company,
item_tax_template: item.item_tax_template,
as_json: true
},
@@ -2534,7 +2527,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.call({
method: "erpnext.stock.get_item_details.get_blanket_order_details",
args: {
ctx:{
args:{
item_code: item.item_code,
customer: doc.customer,
supplier: doc.supplier,

View File

@@ -33,7 +33,7 @@ erpnext.financial_statements = {
return value;
} else if (frappe.query_report.get_filter_value("selected_view") == "Margin" && data) {
if (column.fieldname == "account" && data.account_name == __("Income")) {
if (column.fieldname == "stub" && data.account_name == __("Income")) {
//Taking the total income from each column (for all the financial years) as the base (100%)
this.baseData = row;
}
@@ -52,7 +52,7 @@ erpnext.financial_statements = {
}
}
if (data && column.fieldname == "account") {
if (data && column.fieldname == "stub") {
// first column
value = data.section_name || data.account_name || value;

Some files were not shown because too many files have changed in this diff Show More