Merge pull request #44887 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-12-25 09:05:15 +05:30
committed by GitHub
60 changed files with 7403 additions and 232 deletions

View File

@@ -10,6 +10,7 @@ WEBSITE_REPOS = [
DOCUMENTATION_DOMAINS = [
"docs.erpnext.com",
"docs.frappe.io",
"frappeframework.com",
]

View File

@@ -48,7 +48,7 @@ class BankAccount(Document):
self.name = self.account_name + " - " + self.bank
def on_trash(self):
delete_contact_and_address("BankAccount", self.name)
delete_contact_and_address("Bank Account", self.name)
def validate(self):
self.validate_company()

View File

@@ -117,9 +117,9 @@ class BankClearance(Document):
)
else:
frappe.db.set_value(
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
)
# using db_set to trigger notification
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
payment_entry.db_set("clearance_date", d.clearance_date)
clearance_date_updated = True

View File

@@ -117,11 +117,13 @@ class POSInvoiceMergeLog(Document):
sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
sales_invoice, credit_note = "", ""
sales_invoice_doc = None
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
sales_invoice_doc = self.process_merging_into_sales_invoice(sales)
sales_invoice = sales_invoice_doc.name
if returns:
credit_note = self.process_merging_into_credit_note(returns, sales_invoice)
credit_note = self.process_merging_into_credit_note(returns, sales_invoice_doc)
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
@@ -152,15 +154,23 @@ class POSInvoiceMergeLog(Document):
self.consolidated_invoice = sales_invoice.name
return sales_invoice.name
return sales_invoice
def process_merging_into_credit_note(self, data, sales_invoice):
def process_merging_into_credit_note(self, data, sales_invoice_doc=None):
credit_note = self.get_new_sales_invoice()
credit_note.is_return = 1
credit_note = self.merge_pos_invoice_into(credit_note, data)
referenes = {}
credit_note.return_against = sales_invoice
if sales_invoice_doc:
credit_note.return_against = sales_invoice_doc.name
for d in sales_invoice_doc.items:
referenes[d.item_code] = d.name
for d in credit_note.items:
d.sales_invoice_item = referenes.get(d.item_code)
credit_note.is_consolidated = 1
credit_note.set_posting_time = 1
@@ -366,7 +376,12 @@ class POSInvoiceMergeLog(Document):
return []
def cancel_linked_invoices(self):
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
invoices = [self.consolidated_invoice, self.consolidated_credit_note]
if not invoices:
return
invoices.reverse()
for si_name in invoices:
if not si_name:
continue
si = frappe.get_doc("Sales Invoice", si_name)

View File

@@ -1677,7 +1677,12 @@ class PurchaseInvoice(BuyingController):
if pi:
pi = pi[0][0]
frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi))
frappe.throw(
_("Supplier Invoice No exists in Purchase Invoice {0}").format(
get_link_to_form("Purchase Invoice", pi)
)
)
def update_billing_status_in_pr(self, update_modified=True):
if self.is_return and not self.update_billed_amount_in_purchase_receipt:

View File

@@ -1838,6 +1838,52 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
def test_adjust_incoming_rate_for_rejected_item(self):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
# Cost of Item is zero in Purchase Receipt
pr = make_purchase_receipt(qty=1, rejected_qty=1, rate=0)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 0)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.qty = 1
row.rate = 150
pi.save()
pi.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "warehouse": pi.items[0].warehouse},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 150)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"warehouse": pi.items[0].rejected_warehouse,
},
"stock_value_difference",
)
self.assertFalse(stock_value_difference)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
def test_item_less_defaults(self):
pi = frappe.new_doc("Purchase Invoice")
pi.supplier = "_Test Supplier"

View File

@@ -14,7 +14,8 @@
"advance_amount",
"allocated_amount",
"exchange_gain_loss",
"ref_exchange_rate"
"ref_exchange_rate",
"difference_posting_date"
],
"fields": [
{
@@ -30,7 +31,7 @@
"width": "180px"
},
{
"columns": 3,
"columns": 2,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
@@ -40,7 +41,7 @@
"read_only": 1
},
{
"columns": 3,
"columns": 2,
"fieldname": "remarks",
"fieldtype": "Text",
"in_list_view": 1,
@@ -111,13 +112,20 @@
"label": "Reference Exchange Rate",
"non_negative": 1,
"read_only": 1
},
{
"columns": 2,
"fieldname": "difference_posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Difference Posting Date"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-06-23 21:13:18.013816",
"modified": "2024-12-20 12:04:46.729972",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Advance",

View File

@@ -16,6 +16,7 @@ class PurchaseInvoiceAdvance(Document):
advance_amount: DF.Currency
allocated_amount: DF.Currency
difference_posting_date: DF.Date | None
exchange_gain_loss: DF.Currency
parent: DF.Data
parentfield: DF.Data

View File

@@ -1002,9 +1002,9 @@ class SalesInvoice(SellingController):
def validate_pos(self):
if self.is_return:
invoice_total = self.rounded_total or self.grand_total
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > 1.0 / (
10.0 ** (self.precision("grand_total") + 1.0)
):
if abs(flt(self.paid_amount)) + abs(flt(self.write_off_amount)) - abs(
flt(invoice_total)
) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)):
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
def validate_warehouse(self):

View File

@@ -14,7 +14,8 @@
"advance_amount",
"allocated_amount",
"exchange_gain_loss",
"ref_exchange_rate"
"ref_exchange_rate",
"difference_posting_date"
],
"fields": [
{
@@ -30,7 +31,7 @@
"width": "250px"
},
{
"columns": 3,
"columns": 2,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
@@ -41,7 +42,7 @@
"read_only": 1
},
{
"columns": 3,
"columns": 2,
"fieldname": "remarks",
"fieldtype": "Text",
"in_list_view": 1,
@@ -112,13 +113,20 @@
"label": "Reference Exchange Rate",
"non_negative": 1,
"read_only": 1
},
{
"columns": 2,
"fieldname": "difference_posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Difference Posting Date"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-06-23 21:12:57.557731",
"modified": "2024-12-20 11:58:28.962370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Advance",

View File

@@ -16,6 +16,7 @@ class SalesInvoiceAdvance(Document):
advance_amount: DF.Currency
allocated_amount: DF.Currency
difference_posting_date: DF.Date | None
exchange_gain_loss: DF.Currency
parent: DF.Data
parentfield: DF.Data

View File

@@ -247,14 +247,14 @@ def get_tax_row_for_tds(tax_details, tax_amount):
}
def get_lower_deduction_certificate(company, tax_details, pan_no):
def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
ldc_name = frappe.db.get_value(
"Lower Deduction Certificate",
{
"pan_no": pan_no,
"tax_withholding_category": tax_details.tax_withholding_category,
"valid_from": (">=", tax_details.from_date),
"valid_upto": ("<=", tax_details.to_date),
"valid_from": ("<=", posting_date),
"valid_upto": (">=", posting_date),
"company": company,
},
"name",
@@ -302,7 +302,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = 0
if party_type == "Supplier":
ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
if tax_deducted:
net_total = inv.tax_withholding_net_total
if ldc:
@@ -539,7 +539,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
)
supp_credit_amt = supp_jv_credit_amt
supp_credit_amt += inv.tax_withholding_net_total
supp_credit_amt += inv.get("tax_withholding_net_total", 0)
for type in payment_entry_amounts:
if type.payment_type == "Pay":
@@ -551,9 +551,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
if inv.doctype != "Payment Entry":
tax_withholding_net_total = inv.base_tax_withholding_net_total
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
else:
tax_withholding_net_total = inv.tax_withholding_net_total
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
if (threshold and tax_withholding_net_total >= threshold) or (
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold

View File

@@ -134,7 +134,6 @@ class ReceivablePayableReport:
paid_in_account_currency=0.0,
credit_note_in_account_currency=0.0,
outstanding_in_account_currency=0.0,
cost_center=ple.cost_center,
)
def init_voucher_balance(self):
@@ -150,6 +149,9 @@ class ReceivablePayableReport:
if key not in self.voucher_balance:
self.voucher_balance[key] = self.build_voucher_dict(ple)
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
self.voucher_balance[key].cost_center = ple.cost_center
self.get_invoices(ple)
if self.filters.get("group_by_party"):
@@ -275,9 +277,6 @@ class ReceivablePayableReport:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
if not row.cost_center and ple.cost_center:
row.cost_center = str(ple.cost_center)
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)

View File

@@ -350,7 +350,7 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
gl_entries_by_account,
accounts_by_name,
accounts,
ignore_closing_entries=False,
ignore_closing_entries=ignore_closing_entries,
root_type=root_type,
)

View File

@@ -637,6 +637,7 @@ class GrossProfitGenerator:
packed_item_row = row.copy()
packed_item_row.warehouse = packed_item.warehouse
packed_item_row.qty = packed_item.total_qty * -1
packed_item_row.serial_and_batch_bundle = packed_item.serial_and_batch_bundle
buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
return flt(buying_amount, self.currency_precision)
@@ -728,6 +729,7 @@ class GrossProfitGenerator:
"voucher_no": row.parent,
"allow_zero_valuation": True,
"company": self.filters.company,
"item_code": item_code,
}
)
@@ -748,12 +750,13 @@ class GrossProfitGenerator:
.inner_join(purchase_invoice)
.on(purchase_invoice.name == purchase_invoice_item.parent)
.select(
purchase_invoice.name,
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
)
.where(purchase_invoice.docstatus == 1)
.where(purchase_invoice.posting_date <= self.filters.to_date)
.where(purchase_invoice_item.item_code == item_code)
.where(purchase_invoice.is_return == 0)
.where(purchase_invoice_item.parenttype == "Purchase Invoice")
)
if row.project:
@@ -996,6 +999,7 @@ class GrossProfitGenerator:
"is_return": row.is_return,
"cost_center": row.cost_center,
"invoice": row.parent,
"serial_and_batch_bundle": row.serial_and_batch_bundle,
}
)
@@ -1047,6 +1051,7 @@ class GrossProfitGenerator:
pki.rate,
(pki.rate * pki.qty).as_("base_amount"),
pki.parent_detail_docname,
pki.serial_and_batch_bundle,
)
.where(pki.docstatus == 1)
)

View File

@@ -70,10 +70,10 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
rate = tax_rate_map.get(tax_withholding_category)
if net_total_map.get((voucher_type, name)):
if voucher_type == "Journal Entry":
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
if rate:
total_amount = grand_total = base_total = tax_amount / (rate / 100)
base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0])
total_amount = grand_total = base_total
elif voucher_type == "Purchase Invoice":
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
(voucher_type, name)
@@ -405,7 +405,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
"paid_amount_after_tax",
"base_paid_amount",
],
"Journal Entry": ["total_amount"],
"Journal Entry": ["tax_withholding_category", "total_debit"],
}
entries = frappe.get_all(
@@ -427,7 +427,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
elif doctype == "Payment Entry":
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
else:
value = [entry.total_amount] * 3
value = [entry.total_debit] * 3
net_total_map[(doctype, entry.name)] = value

View File

@@ -308,12 +308,14 @@ class Asset(AccountsController):
)
def validate_precision(self):
float_precision = cint(frappe.db.get_default("float_precision")) or 2
if self.gross_purchase_amount:
self.gross_purchase_amount = flt(self.gross_purchase_amount, float_precision)
self.gross_purchase_amount = flt(
self.gross_purchase_amount, self.precision("gross_purchase_amount")
)
if self.opening_accumulated_depreciation:
self.opening_accumulated_depreciation = flt(
self.opening_accumulated_depreciation, float_precision
self.opening_accumulated_depreciation, self.precision("opening_accumulated_depreciation")
)
def validate_asset_values(self):
@@ -487,11 +489,7 @@ class Asset(AccountsController):
def validate_expected_value_after_useful_life(self):
for row in self.get("finance_books"):
row.expected_value_after_useful_life = flt(
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
)
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
if not depr_schedule:
continue

View File

@@ -889,7 +889,7 @@ class TestDepreciationMethods(AssetSetup):
["2030-12-31", 28630.14, 28630.14],
["2031-12-31", 35684.93, 64315.07],
["2032-12-31", 17842.46, 82157.53],
["2033-06-06", 5342.46, 87499.99],
["2033-06-06", 5342.47, 87500.00],
]
schedules = [

View File

@@ -344,7 +344,7 @@ class AssetDepreciationSchedule(Document):
date_of_disposal,
original_schedule_date=schedule_date,
)
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
if depreciation_amount > 0:
self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n)
@@ -430,6 +430,7 @@ class AssetDepreciationSchedule(Document):
if not depreciation_amount:
continue
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
value_after_depreciation = flt(
value_after_depreciation - flt(depreciation_amount),
asset_doc.precision("gross_purchase_amount"),
@@ -443,6 +444,7 @@ class AssetDepreciationSchedule(Document):
depreciation_amount += flt(value_after_depreciation) - flt(
row.expected_value_after_useful_life
)
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
skip_row = True
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
@@ -517,10 +519,13 @@ class AssetDepreciationSchedule(Document):
i - 1
].accumulated_depreciation_amount
else:
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
accumulated_depreciation = flt(
self.opening_accumulated_depreciation,
asset_doc.precision("opening_accumulated_depreciation"),
)
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
value_after_depreciation -= flt(d.depreciation_amount)
value_after_depreciation = flt(value_after_depreciation, d.precision("depreciation_amount"))
# for the last row, if depreciation method = Straight Line
if (
@@ -530,12 +535,11 @@ class AssetDepreciationSchedule(Document):
and not date_of_return
and not row.shift_based
):
depreciation_amount += flt(
d.depreciation_amount += flt(
value_after_depreciation - flt(row.expected_value_after_useful_life),
d.precision("depreciation_amount"),
)
d.depreciation_amount = depreciation_amount
accumulated_depreciation += d.depreciation_amount
d.accumulated_depreciation_amount = flt(
accumulated_depreciation, d.precision("accumulated_depreciation_amount")

View File

@@ -400,11 +400,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
} else {
cur_frm.add_custom_button(
__("Subcontracting Order"),
this.make_subcontracting_order,
__("Create")
);
if (!doc.items.every((item) => item.qty == item.sco_qty)) {
this.frm.add_custom_button(
__("Subcontracting Order"),
() => {
me.make_subcontracting_order();
},
__("Create")
);
}
}
}
}

View File

@@ -867,27 +867,40 @@ def make_inter_company_sales_order(source_name, target_doc=None):
@frappe.whitelist()
def make_subcontracting_order(source_name, target_doc=None, save=False, submit=False, notify=False):
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
if not is_po_fully_subcontracted(source_name):
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
target_doc.save()
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
target_doc.save()
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
try:
target_doc.submit()
except Exception as e:
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
try:
target_doc.submit()
except Exception as e:
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
if notify:
frappe.msgprint(
_("Subcontracting Order {0} created.").format(
get_link_to_form(target_doc.doctype, target_doc.name)
),
indicator="green",
alert=True,
)
if notify:
frappe.msgprint(
_("Subcontracting Order {0} created.").format(
get_link_to_form(target_doc.doctype, target_doc.name)
),
indicator="green",
alert=True,
)
return target_doc
return target_doc
else:
frappe.throw(_("This PO has been fully subcontracted."))
def is_po_fully_subcontracted(po_name):
table = frappe.qb.DocType("Purchase Order Item")
query = (
frappe.qb.from_(table)
.select(table.name)
.where((table.parent == po_name) & (table.qty != table.sco_qty))
)
return not query.run(as_dict=True)
def get_mapped_subcontracting_order(source_name, target_doc=None):
@@ -931,7 +944,8 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
"material_request": "material_request",
"material_request_item": "material_request_item",
},
"field_no_map": [],
"field_no_map": ["qty", "fg_item_qty", "amount"],
"condition": lambda item: item.qty != item.sco_qty,
},
},
target_doc,
@@ -939,12 +953,3 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
)
return target_doc
@frappe.whitelist()
def is_subcontracting_order_created(po_name) -> bool:
return (
True
if frappe.db.exists("Subcontracting Order", {"purchase_order": po_name, "docstatus": ["=", 1]})
else False
)

View File

@@ -1004,7 +1004,7 @@ class TestPurchaseOrder(FrappeTestCase):
)
def update_items(po, qty):
trans_items = [po.items[0].as_dict()]
trans_items = [po.items[0].as_dict().update({"docname": po.items[0].name})]
trans_items[0]["qty"] = qty
trans_items[0]["fg_item_qty"] = qty
trans_items = json.dumps(trans_items, default=str)
@@ -1059,6 +1059,73 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertEqual(po.items[0].qty, 30)
self.assertEqual(po.items[0].fg_item_qty, 30)
def test_new_sc_flow(self):
from erpnext.buying.doctype.purchase_order.purchase_order import make_subcontracting_order
po = create_po_for_sc_testing()
sco = make_subcontracting_order(po.name)
sco.items[0].qty = 5
sco.items.pop(1)
sco.items[1].qty = 25
sco.save()
sco.submit()
# Test - 1: Quantity of Service Items should change based on change in Quantity of its corresponding Finished Goods Item
self.assertEqual(sco.service_items[0].qty, 5)
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
po.reload()
self.assertEqual(po.items[0].sco_qty, 5)
self.assertEqual(po.items[1].sco_qty, 0)
self.assertEqual(po.items[2].sco_qty, 12.5)
# Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity
self.assertEqual(sco.items[0].amount, 2000)
self.assertEqual(sco.service_items[0].amount, 500)
# Test - 4: Service Items should be removed if its corresponding Finished Good line item is deleted
self.assertEqual(len(sco.service_items), 2)
# Test - 5: Service Item quantity calculation should be based upon conversion factor calculated from its corresponding PO Item
self.assertEqual(sco.service_items[1].qty, 12.5)
sco = make_subcontracting_order(po.name)
sco.items[0].qty = 6
# Test - 6: Saving document should not be allowed if Quantity exceeds available Subcontracting Quantity of any Purchase Order Item
self.assertRaises(frappe.ValidationError, sco.save)
sco.items[0].qty = 5
sco.items.pop()
sco.items.pop()
sco.save()
sco.submit()
sco = make_subcontracting_order(po.name)
# Test - 7: Since line item 1 is now fully subcontracted, new SCO should by default only have the remaining 2 line items
self.assertEqual(len(sco.items), 2)
sco.items.pop(0)
sco.save()
sco.submit()
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
po.reload()
self.assertEqual(po.items[2].sco_qty, 25)
sco.cancel()
po.reload()
self.assertEqual(po.items[2].sco_qty, 12.5)
sco = make_subcontracting_order(po.name)
sco.save()
sco.submit()
# Test - 8: Since this PO is now fully subcontracted, creating a new SCO from it should throw error
self.assertRaises(frappe.ValidationError, make_subcontracting_order, po.name)
@change_settings("Buying Settings", {"auto_create_subcontracting_order": 1})
def test_auto_create_subcontracting_order(self):
from erpnext.controllers.tests.test_subcontracting_controller import (
@@ -1124,6 +1191,53 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertEqual(po.per_billed, 100)
def create_po_for_sc_testing():
from erpnext.controllers.tests.test_subcontracting_controller import (
make_bom_for_subcontracted_items,
make_raw_materials,
make_service_items,
make_subcontracted_items,
)
make_subcontracted_items()
make_raw_materials()
make_service_items()
make_bom_for_subcontracted_items()
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 10,
"rate": 100,
"fg_item": "Subcontracted Item SA1",
"fg_item_qty": 10,
},
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 2",
"qty": 20,
"rate": 25,
"fg_item": "Subcontracted Item SA2",
"fg_item_qty": 15,
},
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 3",
"qty": 25,
"rate": 10,
"fg_item": "Subcontracted Item SA3",
"fg_item_qty": 50,
},
]
return create_purchase_order(
rm_items=service_items,
is_subcontracted=1,
supplier_warehouse="_Test Warehouse 1 - _TC",
)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
from erpnext.selling.doctype.customer.test_customer import create_internal_customer

View File

@@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "hash",
"creation": "2013-05-24 19:29:06",
"creation": "2024-12-09 12:54:24.652161",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
@@ -26,6 +26,7 @@
"quantity_and_rate",
"qty",
"stock_uom",
"sco_qty",
"col_break2",
"uom",
"conversion_factor",
@@ -909,13 +910,21 @@
{
"fieldname": "column_break_fyqr",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "sco_qty",
"fieldtype": "Float",
"label": "Subcontracted Quantity",
"no_copy": 1,
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-02-05 11:23:24.859435",
"modified": "2024-12-10 12:11:18.536089",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -80,6 +80,7 @@ class PurchaseOrderItem(Document):
sales_order_item: DF.Data | None
sales_order_packed_item: DF.Data | None
schedule_date: DF.Date
sco_qty: DF.Float
stock_qty: DF.Float
stock_uom: DF.Link
stock_uom_rate: DF.Currency

View File

@@ -379,13 +379,14 @@ class AccountsController(TransactionBase):
== 1
)
).run()
frappe.db.sql(
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
)
frappe.db.sql(
"delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s",
(self.doctype, self.name),
)
gle = frappe.qb.DocType("GL Entry")
frappe.qb.from_(gle).delete().where(
(gle.voucher_type == self.doctype) & (gle.voucher_no == self.name)
).run()
sle = frappe.qb.DocType("Stock Ledger Entry")
frappe.qb.from_(sle).delete().where(
(sle.voucher_type == self.doctype) & (sle.voucher_no == self.name)
).run()
def remove_serial_and_batch_bundle(self):
bundles = frappe.get_all(
@@ -1150,11 +1151,12 @@ class AccountsController(TransactionBase):
def clear_unallocated_advances(self, childtype, parentfield):
self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]}))
frappe.db.sql(
"""delete from `tab{}` where parentfield={} and parent = {}
and allocated_amount = 0""".format(childtype, "%s", "%s"),
(parentfield, self.name),
)
doctype = frappe.qb.DocType(childtype)
frappe.qb.from_(doctype).delete().where(
(doctype.parentfield == parentfield)
& (doctype.parent == self.name)
& (doctype.allocated_amount == 0)
).run()
@frappe.whitelist()
def apply_shipping_rule(self):
@@ -1204,6 +1206,7 @@ class AccountsController(TransactionBase):
"advance_amount": flt(d.amount),
"allocated_amount": allocated_amount,
"ref_exchange_rate": flt(d.exchange_rate), # exchange_rate of advance entry
"difference_posting_date": self.posting_date,
}
if d.get("paid_from"):
advance_row["account"] = d.paid_from
@@ -1214,6 +1217,8 @@ class AccountsController(TransactionBase):
def get_advance_entries(self, include_unallocated=True):
party_account = []
default_advance_account = None
if self.doctype == "Sales Invoice":
party_type = "Customer"
party = self.customer
@@ -1229,10 +1234,14 @@ class AccountsController(TransactionBase):
order_doctype = "Purchase Order"
party_account.append(self.credit_to)
party_account.extend(
get_party_account(party_type, party=party, company=self.company, include_advance=True)
party_accounts = get_party_account(
party_type, party=party, company=self.company, include_advance=True
)
if party_accounts:
party_account.append(party_accounts[0])
default_advance_account = party_accounts[1] if len(party_accounts) == 2 else None
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
journal_entries = get_advance_journal_entries(
@@ -1240,7 +1249,13 @@ class AccountsController(TransactionBase):
)
payment_entries = get_advance_payment_entries_for_regional(
party_type, party, party_account, order_doctype, order_list, include_unallocated
party_type,
party,
party_account,
order_doctype,
order_list,
default_advance_account,
include_unallocated,
)
res = journal_entries + payment_entries
@@ -1497,7 +1512,6 @@ class AccountsController(TransactionBase):
gain_loss_account = frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account"
)
je = create_gain_loss_journal(
self.company,
args.get("difference_posting_date") if args else self.posting_date,
@@ -1583,6 +1597,7 @@ class AccountsController(TransactionBase):
"Company", self.company, "exchange_gain_loss_account"
),
"exchange_gain_loss": flt(d.get("exchange_gain_loss")),
"difference_posting_date": d.get("difference_posting_date"),
}
)
lst.append(args)
@@ -2116,11 +2131,9 @@ class AccountsController(TransactionBase):
for adv in self.advances:
consider_for_total_advance = True
if adv.reference_name == linked_doc_name:
frappe.db.sql(
f"""delete from `tab{self.doctype} Advance`
where name = %s""",
adv.name,
)
doctype = frappe.qb.DocType(self.doctype + " Advance")
frappe.qb.from_(doctype).delete().where(doctype.name == adv.name).run()
consider_for_total_advance = False
if consider_for_total_advance:

View File

@@ -156,7 +156,7 @@ class StockController(AccountsController):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
is_material_issue = False
if self.doctype == "Stock Entry" and self.purpose == "Material Issue":
if self.doctype == "Stock Entry" and self.purpose in ["Material Issue", "Material Transfer"]:
is_material_issue = True
for d in self.get("items"):
@@ -530,7 +530,7 @@ class StockController(AccountsController):
"account": warehouse_account[sle.warehouse]["account"],
"against": expense_account,
"cost_center": item_row.cost_center,
"project": item_row.project or self.get("project"),
"project": sle.get("project") or item_row.project or self.get("project"),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(sle.stock_value_difference, precision),
"is_opening": item_row.get("is_opening")
@@ -550,7 +550,9 @@ class StockController(AccountsController):
"cost_center": item_row.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(sle.stock_value_difference, precision),
"project": item_row.get("project") or self.get("project"),
"project": sle.get("project")
or item_row.get("project")
or self.get("project"),
"is_opening": item_row.get("is_opening")
or self.get("is_opening")
or "No",
@@ -678,23 +680,34 @@ class StockController(AccountsController):
def get_stock_ledger_details(self):
stock_ledger = {}
stock_ledger_entries = frappe.db.sql(
"""
select
name, warehouse, stock_value_difference, valuation_rate,
voucher_detail_no, item_code, posting_date, posting_time,
actual_qty, qty_after_transaction
from
`tabStock Ledger Entry`
where
voucher_type=%s and voucher_no=%s and is_cancelled = 0
""",
(self.doctype, self.name),
as_dict=True,
)
table = frappe.qb.DocType("Stock Ledger Entry")
stock_ledger_entries = (
frappe.qb.from_(table)
.select(
table.name,
table.warehouse,
table.stock_value_difference,
table.valuation_rate,
table.voucher_detail_no,
table.item_code,
table.posting_date,
table.posting_time,
table.actual_qty,
table.qty_after_transaction,
table.project,
)
.where(
(table.voucher_type == self.doctype)
& (table.voucher_no == self.name)
& (table.is_cancelled == 0)
)
).run(as_dict=True)
for sle in stock_ledger_entries:
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
return stock_ledger
def check_expense_account(self, item):
@@ -999,6 +1012,9 @@ class StockController(AccountsController):
elif self.doctype == "Stock Entry" and row.t_warehouse:
qi_required = True # inward stock needs inspection
if row.get("is_scrap_item"):
continue
if qi_required: # validate row only if inspection is required on item level
self.validate_qi_presence(row)
if self.docstatus == 1:
@@ -1758,6 +1774,9 @@ def make_bundle_for_material_transfer(**kwargs):
bundle_doc.calculate_qty_and_amount()
bundle_doc.flags.ignore_permissions = True
bundle_doc.flags.ignore_validate = True
bundle_doc.save(ignore_permissions=True)
if kwargs.do_not_submit:
bundle_doc.save(ignore_permissions=True)
else:
bundle_doc.submit()
return bundle_doc.name

View File

@@ -103,6 +103,19 @@ class SubcontractingController(StockController):
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
)
if (
self.doctype not in "Subcontracting Receipt"
and item.qty
> flt(get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item))
/ item.sc_conversion_factor
):
frappe.throw(
_(
"Row {0}: Item {1}'s quantity cannot be higher than the available quantity."
).format(item.idx, item.item_name)
)
item.amount = item.qty * item.rate
if item.bom:
is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"])
@@ -1110,6 +1123,12 @@ def get_item_details(items):
return item_details
def get_pending_sco_qty(po_name):
table = frappe.qb.DocType("Purchase Order Item")
query = frappe.qb.from_(table).select(table.name, table.qty, table.sco_qty).where(table.parent == po_name)
return {item.name: item.qty - item.sco_qty for item in query.run(as_dict=True)}
@frappe.whitelist()
def make_rm_stock_entry(
subcontract_order, rm_items=None, order_doctype="Subcontracting Order", target_doc=None

View File

@@ -2,6 +2,8 @@
# For license information, please see license.txt
from datetime import datetime
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
@@ -1979,3 +1981,95 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(len(exc_je_for_adv), 0)
self.remove_advance_accounts_from_party_master()
def test_difference_posting_date_in_pi_and_si(self):
self.setup_advance_accounts_in_party_master()
# create payment entry for customer
adv = self.create_payment_entry(amount=1, source_exc_rate=83)
adv.save()
self.assertEqual(adv.paid_from, self.advance_received_usd)
adv.submit()
adv.reload()
# create sales invoice with advance received
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True)
si.debit_to = self.debtors_usd
si.append(
"advances",
{
"reference_type": adv.doctype,
"reference_name": adv.name,
"remarks": "Amount INR 1 received from _Test MC Customer USD\nTransaction reference no Test001 dated 2024-12-19",
"advance_amount": 1.0,
"allocated_amount": 1.0,
"exchange_gain_loss": 3.0,
"ref_exchange_rate": 83.0,
"difference_posting_date": add_days(nowdate(), -2),
},
)
si.save().submit()
# exc Gain/Loss journal should've been creatad
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
self.assertEqual(len(exc_je_for_si), 1)
self.assertEqual(len(exc_je_for_adv), 1)
self.assertEqual(exc_je_for_si, exc_je_for_adv)
# check jv created with difference_posting_date in sales invoice
jv = frappe.get_doc("Journal Entry", exc_je_for_si[0].parent)
sales_invoice = frappe.get_doc("Sales Invoice", si.name)
self.assertEqual(sales_invoice.advances[0].difference_posting_date, jv.posting_date)
# create payment entry for supplier
usd_amount = 1
inr_amount = 85
exc_rate = 85
adv = create_payment_entry(
company=self.company,
payment_type="Pay",
party_type="Supplier",
party=self.supplier,
paid_from=self.cash,
paid_to=self.advance_paid_usd,
paid_amount=inr_amount,
)
adv.source_exchange_rate = 1
adv.target_exchange_rate = exc_rate
adv.received_amount = usd_amount
adv.paid_amount = exc_rate * usd_amount
adv.posting_date = nowdate()
adv.save()
self.assertEqual(adv.paid_to, self.advance_paid_usd)
adv.submit()
# create purchase invoice with advance paid
pi = self.create_purchase_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True)
pi.append(
"advances",
{
"reference_type": adv.doctype,
"reference_name": adv.name,
"remarks": "Amount INR 1 paid to _Test MC Supplier USD\nTransaction reference no Test001 dated 2024-12-20",
"advance_amount": 1.0,
"allocated_amount": 1.0,
"exchange_gain_loss": 5.0,
"ref_exchange_rate": 85.0,
"difference_posting_date": add_days(nowdate(), -2),
},
)
pi.save().submit()
self.assertEqual(pi.credit_to, self.creditors_usd)
# exc Gain/Loss journal should've been creatad
exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name)
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
self.assertEqual(len(exc_je_for_pi), 1)
self.assertEqual(len(exc_je_for_adv), 1)
self.assertEqual(exc_je_for_pi, exc_je_for_adv)
# check jv created with difference_posting_date in purchase invoice
journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent)
purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name)
self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date)

View File

@@ -1261,6 +1261,7 @@ def make_raw_materials():
for item, properties in raw_materials.items():
if not frappe.db.exists("Item", item):
properties.update({"is_stock_item": 1})
properties.update({"valuation_rate": 100})
make_item(item, properties)
@@ -1311,7 +1312,7 @@ def make_bom_for_subcontracted_items():
for item_code, raw_materials in boms.items():
if not frappe.db.exists("BOM", {"item": item_code}):
make_bom(item=item_code, raw_materials=raw_materials, rate=100)
make_bom(item=item_code, raw_materials=raw_materials, rate=100, currency="INR")
def set_backflush_based_on(based_on):

View File

@@ -38,7 +38,7 @@
"table_fieldname": "competitors"
}
],
"modified": "2023-11-23 19:33:54.284279",
"modified": "2024-12-10 08:26:38.496003",
"modified_by": "Administrator",
"module": "CRM",
"name": "Competitor",
@@ -53,20 +53,25 @@
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"role": "Sales Master Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"write": 1
"role": "Sales User"
},
{
"read": 1,
"role": "Sales Manager"
},
{
"read": 1,
"role": "Maintenance Manager"
},
{
"read": 1,
"role": "Maintenance User"
}
],
"quick_entry": 1,

View File

@@ -25,7 +25,12 @@ frappe.ui.form.on("Plaid Settings", {
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
freeze: true,
callback: () => {
let bank_transaction_link = '<a href="#List/Bank Transaction">Bank Transaction</a>';
let bank_transaction_link = frappe.utils.get_form_link(
"Bank Transaction",
"",
true,
"Bank Transaction"
);
frappe.msgprint({
title: __("Sync Started"),

View File

@@ -944,8 +944,9 @@ class JobCard(Document):
if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
min_qty = []
for d in doc.operations:
if d.completed_qty:
min_qty.append(d.completed_qty)
completed_qty = flt(d.completed_qty) + flt(d.process_loss_qty)
if completed_qty:
min_qty.append(completed_qty)
else:
min_qty = []
break

View File

@@ -7,6 +7,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings, timeout
from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, today
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
from erpnext.manufacturing.doctype.job_card.job_card import make_stock_entry as make_stock_entry_from_jc
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.manufacturing.doctype.work_order.work_order import (
CapacityError,
@@ -505,6 +506,60 @@ class TestWorkOrder(FrappeTestCase):
for stock_entry in stock_entries:
stock_entry.cancel()
def test_work_order_material_transferred_qty_with_process_loss(self):
stock_entries = []
bom = frappe.get_doc("BOM", {"docstatus": 1, "with_operations": 1, "company": "_Test Company"})
work_order = make_wo_order_test_record(
item=bom.item,
qty=2,
bom_no=bom.name,
source_warehouse="_Test Warehouse - _TC",
transfer_material_against="Job Card",
)
self.assertEqual(work_order.qty, 2)
for row in work_order.required_items:
stock_entry_doc = test_stock_entry.make_stock_entry(
item_code=row.item_code, target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100
)
stock_entries.append(stock_entry_doc)
job_cards = frappe.get_all(
"Job Card", filters={"work_order": work_order.name}, order_by="creation asc"
)
for row in job_cards:
transfer_entry_1 = make_stock_entry_from_jc(row.name)
transfer_entry_1.submit()
doc = frappe.get_doc("Job Card", row.name)
for row in doc.scheduled_time_logs:
doc.append(
"time_logs",
{
"from_time": row.from_time,
"to_time": row.to_time,
"time_in_mins": row.time_in_mins,
"completed_qty": 1,
},
)
doc.save()
doc.submit()
self.assertEqual(doc.total_completed_qty, 1)
self.assertEqual(doc.process_loss_qty, 1)
work_order.reload()
self.assertEqual(work_order.material_transferred_for_manufacturing, 2)
for row in work_order.operations:
self.assertEqual(row.completed_qty, 1)
self.assertEqual(row.process_loss_qty, 1)
def test_capcity_planning(self):
frappe.db.set_single_value(
"Manufacturing Settings", {"disable_capacity_planning": 0, "capacity_planning_for_days": 1}

View File

@@ -574,11 +574,19 @@ erpnext.PointOfSale.Controller = class {
} else {
if (!this.frm.doc.customer) return this.raise_customer_selection_alert();
const { item_code, batch_no, serial_no, rate, uom } = item;
const { item_code, batch_no, serial_no, rate, uom, stock_uom } = item;
if (!item_code) return;
const new_item = { item_code, batch_no, rate, uom, [field]: value };
if (rate == undefined || rate == 0) {
frappe.show_alert({
message: __("Price is not set for the item."),
indicator: "orange",
});
frappe.utils.play_sound("error");
return;
}
const new_item = { item_code, batch_no, rate, uom, [field]: value, stock_uom };
if (serial_no) {
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);

View File

@@ -118,6 +118,7 @@ erpnext.PointOfSale.ItemSelector = class {
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}"
data-rate="${escape(price_list_rate || 0)}"
data-stock-uom="${escape(item.stock_uom)}"
title="${item.item_name}">
${get_item_image_html()}
@@ -251,17 +252,19 @@ erpnext.PointOfSale.ItemSelector = class {
let serial_no = unescape($item.attr("data-serial-no"));
let uom = unescape($item.attr("data-uom"));
let rate = unescape($item.attr("data-rate"));
let stock_uom = unescape($item.attr("data-stock-uom"));
// escape(undefined) returns "undefined" then unescape returns "undefined"
batch_no = batch_no === "undefined" ? undefined : batch_no;
serial_no = serial_no === "undefined" ? undefined : serial_no;
uom = uom === "undefined" ? undefined : uom;
rate = rate === "undefined" ? undefined : rate;
stock_uom = stock_uom === "undefined" ? undefined : stock_uom;
me.events.item_selected({
field: "qty",
value: "+1",
item: { item_code, batch_no, serial_no, uom, rate },
item: { item_code, batch_no, serial_no, uom, rate, stock_uom },
});
me.search_field.set_focus();
});

View File

@@ -32,7 +32,7 @@
"table_fieldname": "lost_reasons"
}
],
"modified": "2023-11-23 19:31:02.743353",
"modified": "2024-12-10 08:21:38.280627",
"modified_by": "Administrator",
"module": "Setup",
"name": "Quotation Lost Reason",
@@ -49,6 +49,22 @@
"role": "Sales Master Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "Sales User"
},
{
"read": 1,
"role": "Sales Manager"
},
{
"read": 1,
"role": "Maintenance User"
},
{
"read": 1,
"role": "Maintenance Manager"
}
],
"quick_entry": 1,

View File

@@ -4500,9 +4500,200 @@
},
"Sweden": {
"Sweden Tax": {
"account_name": "VAT",
"tax_rate": 25.00
"tax_categories": [],
"chart_of_accounts": {
"*": {
"sales_tax_templates": [
{
"title": "Försäljning Moms 25%",
"is_default": 0,
"taxes": [
{
"account_head": {
"account_name": "Utgående moms, 25 %",
"account_number": "2610",
"tax_rate": 25.00
},
"description": "Moms 25%",
"rate": 25.00
}
]
},
{
"title": "Försäljning Moms 12%",
"is_default": 0,
"taxes": [
{
"account_head": {
"account_name": "Utgående moms, 12 %",
"account_number": "2620",
"tax_rate": 12.00
},
"description": "Moms 12%",
"rate": 12.00
}
]
},
{
"title": "Försäljning Moms 6%",
"is_default": 0,
"taxes": [
{
"account_head": {
"account_name": "Utgående moms, 6 %",
"account_number": "2630",
"tax_rate": 6.00
},
"description": "Moms 6%",
"rate": 6.00
}
]
},
{
"title": "Försäljning Moms 0%",
"is_default": 0,
"taxes": [
{
"account_head": {
"account_name": "Utgående moms, 6 %",
"account_number": "2630",
"tax_rate": 0.00
},
"description": "Moms 0%",
"rate": 0.00
}
]
}
],
"purchase_tax_templates": [
{
"title": "Inköp Moms 25%",
"is_default": 0,
"taxes": [
{
"account_head": {
"account_name": "Ingående moms",
"account_number": "2640",
"root_type": "Liability",
"tax_rate": 25.00
},
"description": "Moms 25%",
"rate": 25.00
}
]
},
{
"title": "Inköp Moms 12%",
"is_default": 0,
"taxes": [
{
"account_head": {
"account_name": "Ingående moms",
"account_number": "2640",
"root_type": "Liability",
"tax_rate": 12.00
},
"description": "Moms 12%",
"rate": 12.00
}
]
},
{
"title": "Inköp Moms 6%",
"is_default": 0,
"taxes": [
{
"account_head": {
"account_name": "Ingående moms",
"account_number": "2640",
"root_type": "Liability",
"tax_rate": 6.00
},
"description": "Moms 6%",
"rate": 6.00
}
]
},
{
"title": "Inköp Moms 0%",
"is_default": 0,
"taxes": [
{
"account_head": {
"account_name": "Ingående moms",
"account_number": "2640",
"root_type": "Liability",
"tax_rate": 0.00
},
"description": "Moms 0%",
"rate": 0.00
}
]
}
],
"item_tax_templates": [
{
"title": "Artikel Moms 25%",
"taxes": [
{
"tax_type": {
"account_name": "Utgående moms, 25 %",
"account_number": "2610",
"root_type": "Liability",
"tax_rate": 25.00
},
"description": "Moms 25%",
"tax_rate": 25.00
}
]
},
{
"title": "Artikel Moms 12%",
"taxes": [
{
"tax_type": {
"account_name": "Utgående moms, 12 %",
"account_number": "2620",
"root_type": "Liability",
"tax_rate": 12.00
},
"description": "Moms 12%",
"tax_rate": 12.00
}
]
},
{
"title": "Artikel Moms 6%",
"taxes": [
{
"tax_type": {
"account_name": "Utgående moms, 6 %",
"account_number": "2630",
"root_type": "Liability",
"tax_rate": 6.00
},
"description": "Moms 6%",
"tax_rate": 6.00
}
]
},
{
"title": "Artikel Moms 0%",
"taxes": [
{
"tax_type": {
"account_name": "Utgående moms, 0 %",
"account_number": "2611",
"root_type": "Liability",
"tax_rate": 0.00
},
"description": "Moms 0%",
"tax_rate": 0.00
}
]
}
]
}
}
},

View File

@@ -1,19 +1,37 @@
{
"chart_name": "Warehouse wise Stock Value",
"chart_type": "Custom",
"creation": "2020-07-20 21:01:04.296157",
"creation": "2022-03-30 00:58:02.018824",
"docstatus": 0,
"doctype": "Dashboard Chart",
"filters_json": "{}",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"modified": "2020-07-22 13:01:01.815123",
"last_synced_on": "2024-12-23 18:44:46.822164",
"modified": "2024-12-23 19:31:17.003946",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse wise Stock Value",
"number_of_groups": 0,
"owner": "Administrator",
"roles": [
{
"role": "Sales Manager"
},
{
"role": "Accounts Manager"
},
{
"role": "Stock Manager"
},
{
"role": "Stock User"
},
{
"role": "Accounts User"
}
],
"source": "Warehouse wise Stock Value",
"timeseries": 0,
"type": "Bar",

View File

@@ -3,7 +3,7 @@ from collections import defaultdict
import frappe
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import flt
from frappe.utils import flt, nowtime
from frappe.utils.deprecations import deprecated
from pypika import Order
@@ -112,7 +112,10 @@ class DeprecatedBatchNoValuation:
sle = frappe.qb.DocType("Stock Ledger Entry")
timestamp_condition = None
if self.sle.posting_date and self.sle.posting_time:
if self.sle.posting_date:
if self.sle.posting_time is None:
self.sle.posting_time = nowtime()
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
if not self.sle.creation:
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)

View File

@@ -113,7 +113,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-05-18 11:46:04.448220",
"modified": "2024-12-19 13:48:46.618066",
"modified_by": "Administrator",
"module": "Stock",
"name": "Closing Stock Balance",
@@ -121,6 +121,7 @@
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -130,10 +131,39 @@
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock User",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -1247,6 +1247,7 @@ def create_stock_entry(pick_list):
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.pick_list = pick_list.get("name")
stock_entry.purpose = pick_list.get("purpose")
stock_entry.company = pick_list.get("company")
stock_entry.set_stock_entry_type()
if pick_list.get("work_order"):

View File

@@ -920,12 +920,17 @@ class PurchaseReceipt(BuyingController):
)
def enable_recalculate_rate_in_sles(self):
rejected_warehouses = frappe.get_all(
"Purchase Receipt Item", filters={"parent": self.name}, pluck="rejected_warehouse"
)
sle_table = frappe.qb.DocType("Stock Ledger Entry")
(
frappe.qb.update(sle_table)
.set(sle_table.recalculate_rate, 1)
.where(sle_table.voucher_no == self.name)
.where(sle_table.voucher_type == "Purchase Receipt")
.where(sle_table.warehouse.notin(rejected_warehouses))
).run()

View File

@@ -90,12 +90,10 @@ class SerialandBatchBundle(Document):
self.validate_duplicate_serial_and_batch_no()
self.validate_voucher_no()
if self.docstatus == 0:
self.allow_existing_serial_nos()
if self.type_of_transaction == "Maintenance":
return
self.allow_existing_serial_nos()
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
self.validate_serial_nos_duplicate()
self.check_future_entries_exists()
@@ -809,7 +807,7 @@ class SerialandBatchBundle(Document):
)
for serial_no, batch_no in serial_batches.items():
if correct_batches.get(serial_no) != batch_no:
if correct_batches.get(serial_no) and correct_batches.get(serial_no) != batch_no:
self.throw_error_message(
f"Serial No {bold(serial_no)} does not belong to Batch No {bold(batch_no)}"
)
@@ -1188,19 +1186,19 @@ def parse_csv_file_to_get_serial_batch(reader):
continue
if has_serial_no or (has_serial_no and has_batch_no):
_dict = {"serial_no": row[0], "qty": 1}
_dict = {"serial_no": row[0].strip(), "qty": 1}
if has_batch_no:
_dict.update(
{
"batch_no": row[1],
"batch_no": row[1].strip(),
"qty": row[2],
}
)
batch_nos.append(
{
"batch_no": row[1],
"batch_no": row[1].strip(),
"qty": row[2],
}
)
@@ -1209,7 +1207,7 @@ def parse_csv_file_to_get_serial_batch(reader):
elif has_batch_no:
batch_nos.append(
{
"batch_no": row[0],
"batch_no": row[0].strip(),
"qty": row[1],
}
)
@@ -1253,7 +1251,7 @@ def make_serial_nos(item_code, serial_nos):
"Item", item_code, ["description", "item_code", "item_name", "warranty_period"], as_dict=1
)
serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")]
serial_nos = [d.get("serial_no").strip() for d in serial_nos if d.get("serial_no")]
existing_serial_nos = frappe.get_all("Serial No", filters={"name": ("in", serial_nos)})
existing_serial_nos = [d.get("name") for d in existing_serial_nos if d.get("name")]
@@ -2101,6 +2099,8 @@ def update_available_batches(available_batches, *reserved_batches) -> None:
def get_available_batches(kwargs):
from erpnext.stock.utils import get_combine_datetime
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
batch_table = frappe.qb.DocType("Batch")
@@ -2128,9 +2128,9 @@ def get_available_batches(kwargs):
if kwargs.get("posting_time") is None:
kwargs.posting_time = nowtime()
timestamp_condition = CombineDatetime(
stock_ledger_entry.posting_date, stock_ledger_entry.posting_time
) <= CombineDatetime(kwargs.posting_date, kwargs.posting_time)
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
kwargs.posting_date, kwargs.posting_time
)
query = query.where(timestamp_condition)

View File

@@ -186,6 +186,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
}
)
doc.set_posting_datetime()
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True
@@ -586,6 +587,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
"company": "_Test Company",
}
)
doc.set_posting_datetime()
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True

View File

@@ -371,6 +371,7 @@ frappe.ui.form.on("Stock Entry", {
function () {
frappe.call({
method: "erpnext.stock.doctype.stock_entry.stock_entry.get_expired_batch_items",
freeze: true,
callback: function (r) {
if (!r.exc && r.message) {
frm.set_value("items", []);

View File

@@ -2975,17 +2975,45 @@ def get_uom_details(item_code, uom, qty):
@frappe.whitelist()
def get_expired_batch_items():
return frappe.db.sql(
"""select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\
from `tabBatch` b, `tabStock Ledger Entry` sle
where b.expiry_date <= %s
and b.expiry_date is not NULL
and b.batch_id = sle.batch_no and sle.is_cancelled = 0
group by sle.warehouse, sle.item_code, sle.batch_no""",
(nowdate()),
as_dict=1,
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import get_auto_batch_nos
expired_batches = get_expired_batches()
if not expired_batches:
return []
expired_batches_stock = get_auto_batch_nos(
frappe._dict(
{
"batch_no": list(expired_batches.keys()),
"for_stock_levels": True,
}
)
)
for row in expired_batches_stock:
row.update(expired_batches.get(row.batch_no))
return expired_batches_stock
def get_expired_batches():
batch = frappe.qb.DocType("Batch")
data = (
frappe.qb.from_(batch)
.select(batch.item, batch.name.as_("batch_no"), batch.stock_uom)
.where((batch.expiry_date <= nowdate()) & (batch.expiry_date.isnotnull()))
).run(as_dict=True)
if not data:
return []
expired_batches = frappe._dict()
for row in data:
expired_batches[row.batch_no] = row
return expired_batches
@frappe.whitelist()
def get_warehouse_details(args):

View File

@@ -970,6 +970,80 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(frappe.ValidationError, ste.submit)
def test_quality_check_for_scrap_item(self):
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as _make_stock_entry,
)
scrap_item = "_Test Scrap Item 1"
make_item(scrap_item, {"is_stock_item": 1, "is_purchase_item": 0})
bom_name = frappe.db.get_value("BOM Scrap Item", {"docstatus": 1}, "parent")
production_item = frappe.db.get_value("BOM", bom_name, "item")
work_order = frappe.new_doc("Work Order")
work_order.production_item = production_item
work_order.update(
{
"company": "_Test Company",
"fg_warehouse": "_Test Warehouse 1 - _TC",
"production_item": production_item,
"bom_no": bom_name,
"qty": 1.0,
"stock_uom": frappe.db.get_value("Item", production_item, "stock_uom"),
"skip_transfer": 1,
}
)
work_order.get_items_and_operations_from_bom()
work_order.submit()
stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1))
for row in stock_entry.items:
if row.s_warehouse:
make_stock_entry(
item_code=row.item_code,
target=row.s_warehouse,
qty=row.qty,
basic_rate=row.basic_rate or 100,
)
if row.is_scrap_item:
row.item_code = scrap_item
row.uom = frappe.db.get_value("Item", scrap_item, "stock_uom")
row.stock_uom = frappe.db.get_value("Item", scrap_item, "stock_uom")
stock_entry.inspection_required = 1
stock_entry.save()
self.assertTrue([row.item_code for row in stock_entry.items if row.is_scrap_item])
for row in stock_entry.items:
if not row.is_scrap_item:
qc = frappe.get_doc(
{
"doctype": "Quality Inspection",
"reference_name": stock_entry.name,
"inspected_by": "Administrator",
"reference_type": "Stock Entry",
"inspection_type": "In Process",
"status": "Accepted",
"sample_size": 1,
"item_code": row.item_code,
}
)
qc_name = qc.submit()
row.quality_inspection = qc_name
stock_entry.reload()
stock_entry.submit()
for row in stock_entry.items:
if row.is_scrap_item:
self.assertFalse(row.quality_inspection)
else:
self.assertTrue(row.quality_inspection)
def test_quality_check(self):
item_code = "_Test Item For QC"
if not frappe.db.exists("Item", item_code):

View File

@@ -124,6 +124,7 @@
"options": "DocType",
"print_width": "150px",
"read_only": 1,
"search_index": 1,
"width": "150px"
},
{
@@ -362,7 +363,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-08-27 09:28:03.961443",
"modified": "2024-12-23 18:03:05.171023",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",

View File

@@ -88,6 +88,7 @@ class StockLedgerEntry(Document):
self.flags.ignore_submit_comment = True
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
self.set_posting_datetime()
self.validate_mandatory()
self.validate_batch()
validate_disabled_warehouse(self.warehouse)
@@ -98,15 +99,10 @@ class StockLedgerEntry(Document):
self.validate_with_last_transaction_posting_time()
self.validate_inventory_dimension_negative_stock()
def set_posting_datetime(self, save=False):
def set_posting_datetime(self):
from erpnext.stock.utils import get_combine_datetime
if save:
posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
if not self.posting_datetime or self.posting_datetime != posting_datetime:
self.db_set("posting_datetime", posting_datetime)
else:
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
def validate_inventory_dimension_negative_stock(self):
if self.is_cancelled or self.actual_qty >= 0:
@@ -173,7 +169,6 @@ class StockLedgerEntry(Document):
return inv_dimension_dict
def on_submit(self):
self.set_posting_datetime(save=True)
self.check_stock_frozen_date()
# Added to handle few test cases where serial_and_batch_bundles are not required

View File

@@ -1242,6 +1242,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
}
)
doc.set_posting_datetime()
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True

View File

@@ -643,7 +643,10 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
child = frappe.qb.DocType("Serial and Batch Entry")
timestamp_condition = ""
if self.sle.posting_date and self.sle.posting_time:
if self.sle.posting_date:
if self.sle.posting_time is None:
self.sle.posting_time = nowtime()
timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) < CombineDatetime(
self.sle.posting_date, self.sle.posting_time
)
@@ -725,15 +728,6 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
self.stock_value_change += stock_value_change
frappe.db.set_value(
"Serial and Batch Entry",
ledger.name,
{
"stock_value_difference": stock_value_change,
"incoming_rate": self.batch_avg_rate[batch_no],
},
)
def calculate_valuation_rate(self):
if not hasattr(self, "wh_data"):
return
@@ -947,12 +941,13 @@ class SerialBatchCreation:
if self.get("make_bundle_from_sle") and self.type_of_transaction == "Inward":
doc.flags.ignore_validate_serial_batch = True
doc.save()
self.validate_qty(doc)
if not hasattr(self, "do_not_submit") or not self.do_not_submit:
doc.flags.ignore_voucher_validation = True
doc.submit()
else:
doc.save()
self.validate_qty(doc)
return doc

View File

@@ -222,7 +222,6 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
sle.flags.ignore_permissions = 1
sle.allow_negative_stock = allow_negative_stock
sle.via_landed_cost_voucher = via_landed_cost_voucher
sle.set_posting_datetime()
sle.submit()
# Added to handle the case when the stock ledger entry is created from the repostig
@@ -988,18 +987,23 @@ class update_entries_after:
if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle):
return
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
doc.calculate_qty_and_amount(save=True)
if self.args.get("sle_id") and sle.actual_qty < 0:
doc = frappe.db.get_value(
"Serial and Batch Bundle",
sle.serial_and_batch_bundle,
["total_amount", "total_qty"],
as_dict=1,
)
else:
doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
doc.calculate_qty_and_amount(save=True)
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
precision = doc.precision("total_qty")
self.wh_data.qty_after_transaction += flt(doc.total_qty, precision)
if flt(self.wh_data.qty_after_transaction, precision):
self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt(
self.wh_data.qty_after_transaction, precision
self.wh_data.qty_after_transaction += flt(doc.total_qty, self.flt_precision)
if flt(self.wh_data.qty_after_transaction, self.flt_precision):
self.wh_data.valuation_rate = flt(self.wh_data.stock_value, self.flt_precision) / flt(
self.wh_data.qty_after_transaction, self.flt_precision
)
def update_valuation_rate_in_serial_and_batch_bundle(self, sle, valuation_rate):

View File

@@ -5,10 +5,38 @@ frappe.provide("erpnext.buying");
erpnext.landed_cost_taxes_and_charges.setup_triggers("Subcontracting Order");
// client script for Subcontracting Order Item is not necessarily required as the server side code will do everything that is necessary.
// this is just so that the user does not get potentially confused
frappe.ui.form.on("Subcontracting Order Item", {
qty(frm, cdt, cdn) {
const row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, "amount", row.qty * row.rate);
const service_item = frm.doc.service_items[row.idx - 1];
frappe.model.set_value(
service_item.doctype,
service_item.name,
"qty",
row.qty * row.sc_conversion_factor
);
frappe.model.set_value(service_item.doctype, service_item.name, "fg_item_qty", row.qty);
frappe.model.set_value(
service_item.doctype,
service_item.name,
"amount",
row.qty * row.sc_conversion_factor * service_item.rate
);
},
before_items_remove(frm, cdt, cdn) {
const row = locals[cdt][cdn];
frm.toggle_enable(["service_items"], true);
frm.get_field("service_items").grid.grid_rows[row.idx - 1].remove();
frm.toggle_enable(["service_items"], false);
},
});
frappe.ui.form.on("Subcontracting Order", {
setup: (frm) => {
frm.get_field("items").grid.cannot_add_rows = true;
frm.get_field("items").grid.only_sortable();
frm.trigger("set_queries");
frm.set_indicator_formatter("item_code", (doc) => (doc.qty <= doc.received_qty ? "green" : "orange"));

View File

@@ -6,7 +6,6 @@ from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_balance import update_bin_qty
@@ -120,20 +119,15 @@ class SubcontractingOrder(SubcontractingController):
def on_submit(self):
self.update_prevdoc_status()
self.update_status()
self.update_sco_qty_in_po()
def on_cancel(self):
self.update_prevdoc_status()
self.update_status()
self.update_sco_qty_in_po(cancel=True)
def validate_purchase_order_for_subcontracting(self):
if self.purchase_order:
if is_subcontracting_order_created(self.purchase_order):
frappe.throw(
_(
"Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one."
)
)
po = frappe.get_doc("Purchase Order", self.purchase_order)
if not po.is_subcontracted:
@@ -154,10 +148,23 @@ class SubcontractingOrder(SubcontractingController):
frappe.throw(_("Please select a Subcontracting Purchase Order."))
def validate_service_items(self):
for item in self.service_items:
if frappe.get_value("Item", item.item_code, "is_stock_item"):
msg = f"Service Item {item.item_name} must be a non-stock item."
frappe.throw(_(msg))
purchase_order_items = [item.purchase_order_item for item in self.items]
self.service_items = [
service_item
for service_item in self.service_items
if service_item.purchase_order_item in purchase_order_items
]
for service_item in self.service_items:
if frappe.get_value("Item", service_item.item_code, "is_stock_item"):
frappe.throw(_("Service Item {0} must be a non-stock item.").format(service_item.item_code))
item = next(
item for item in self.items if item.purchase_order_item == service_item.purchase_order_item
)
service_item.qty = item.qty * item.sc_conversion_factor
service_item.fg_item_qty = item.qty
service_item.amount = service_item.qty * service_item.rate
def validate_supplied_items(self):
if self.supplier_warehouse:
@@ -241,6 +248,18 @@ class SubcontractingOrder(SubcontractingController):
for si in self.service_items:
if si.fg_item:
item = frappe.get_doc("Item", si.fg_item)
po_item = frappe.get_doc("Purchase Order Item", si.purchase_order_item)
available_qty = po_item.qty - po_item.sco_qty
if available_qty == 0:
continue
si.qty = available_qty
conversion_factor = po_item.qty / po_item.fg_item_qty
si.fg_item_qty = available_qty / conversion_factor
si.amount = available_qty * si.rate
bom = (
frappe.db.get_value(
"Subcontracting BOM",
@@ -257,6 +276,7 @@ class SubcontractingOrder(SubcontractingController):
"schedule_date": self.schedule_date,
"description": item.description,
"qty": si.fg_item_qty,
"sc_conversion_factor": conversion_factor,
"stock_uom": item.stock_uom,
"bom": bom,
"purchase_order_item": si.purchase_order_item,
@@ -310,6 +330,12 @@ class SubcontractingOrder(SubcontractingController):
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
def update_sco_qty_in_po(self, cancel=False):
for service_item in self.service_items:
doc = frappe.get_doc("Purchase Order Item", service_item.purchase_order_item)
doc.sco_qty = (doc.sco_qty + service_item.qty) if not cancel else (doc.sco_qty - service_item.qty)
doc.save()
@frappe.whitelist()
def make_subcontracting_receipt(source_name, target_doc=None):

View File

@@ -40,12 +40,6 @@ class TestSubcontractingOrder(FrappeTestCase):
make_service_items()
make_bom_for_subcontracted_items()
def test_populate_items_table(self):
sco = get_subcontracting_order()
sco.items = None
sco.populate_items_table()
self.assertEqual(len(sco.service_items), len(sco.items))
def test_set_missing_values(self):
sco = get_subcontracting_order()
before = {sco.total_qty, sco.total, sco.total_additional_costs}

View File

@@ -51,7 +51,8 @@
"project",
"section_break_34",
"purchase_order_item",
"page_break"
"page_break",
"sc_conversion_factor"
],
"fields": [
{
@@ -144,8 +145,8 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity",
"non_negative": 1,
"print_width": "60px",
"read_only": 1,
"reqd": 1,
"width": "60px"
},
@@ -381,13 +382,20 @@
"no_copy": 1,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "sc_conversion_factor",
"fieldtype": "Float",
"hidden": 1,
"label": "SC Conversion Factor",
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-12-06 15:23:05.252346",
"modified": "2024-12-13 13:35:28.935898",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Item",

View File

@@ -42,6 +42,7 @@ class SubcontractingOrderItem(Document):
received_qty: DF.Float
returned_qty: DF.Float
rm_cost_per_qty: DF.Currency
sc_conversion_factor: DF.Float
schedule_date: DF.Date | None
service_cost_per_qty: DF.Currency
stock_uom: DF.Link

View File

@@ -155,7 +155,7 @@
],
"istable": 1,
"links": [],
"modified": "2023-11-30 13:29:31.017440",
"modified": "2024-12-05 17:33:46.099601",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Service Item",

View File

@@ -19,6 +19,8 @@ class SubcontractingOrderServiceItem(Document):
fg_item_qty: DF.Float
item_code: DF.Link
item_name: DF.Data
material_request: DF.Link | None
material_request_item: DF.Data | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data