mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-18 00:55:02 +00:00
Merge pull request #35664 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -50,13 +50,15 @@ class AccountingDimension(Document):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
|
||||
frappe.enqueue(
|
||||
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
if frappe.flags.in_test:
|
||||
delete_accounting_dimension(doc=self)
|
||||
else:
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
|
||||
|
||||
def set_fieldname_and_label(self):
|
||||
if not self.label:
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"book_tax_discount_loss",
|
||||
"print_settings",
|
||||
"show_inclusive_tax_in_print",
|
||||
"show_taxes_as_table_in_print",
|
||||
"column_break_12",
|
||||
"show_payment_schedule_in_print",
|
||||
"currency_exchange_section",
|
||||
@@ -378,6 +379,12 @@
|
||||
"fieldname": "auto_reconcile_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto Reconcile Payments"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_taxes_as_table_in_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -385,7 +392,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 13:11:37.130743",
|
||||
"modified": "2023-06-13 18:47:46.430291",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -414,4 +421,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,21 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
}
|
||||
},
|
||||
|
||||
validate_rounding_loss: function(frm) {
|
||||
allowance = frm.doc.rounding_loss_allowance;
|
||||
if (!(allowance > 0 && allowance < 1)) {
|
||||
frappe.throw(__("Rounding Loss Allowance should be between 0 and 1"));
|
||||
}
|
||||
},
|
||||
|
||||
rounding_loss_allowance: function(frm) {
|
||||
frm.events.validate_rounding_loss(frm);
|
||||
},
|
||||
|
||||
validate: function(frm) {
|
||||
frm.events.validate_rounding_loss(frm);
|
||||
},
|
||||
|
||||
get_entries: function(frm, account) {
|
||||
frappe.call({
|
||||
method: "get_accounts_data",
|
||||
@@ -126,7 +141,8 @@ var get_account_details = function(frm, cdt, cdn) {
|
||||
company: frm.doc.company,
|
||||
posting_date: frm.doc.posting_date,
|
||||
party_type: row.party_type,
|
||||
party: row.party
|
||||
party: row.party,
|
||||
rounding_loss_allowance: frm.doc.rounding_loss_allowance
|
||||
},
|
||||
callback: function(r){
|
||||
$.extend(row, r.message);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"posting_date",
|
||||
"rounding_loss_allowance",
|
||||
"column_break_2",
|
||||
"company",
|
||||
"section_break_4",
|
||||
@@ -96,11 +97,18 @@
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0.05",
|
||||
"description": "Only values between 0 and 1 are allowed. \nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
|
||||
"fieldname": "rounding_loss_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rounding Loss Allowance"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-29 19:38:24.416529",
|
||||
"modified": "2023-06-12 21:02:09.818208",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Exchange Rate Revaluation",
|
||||
|
||||
@@ -18,8 +18,13 @@ from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
class ExchangeRateRevaluation(Document):
|
||||
def validate(self):
|
||||
self.validate_rounding_loss_allowance()
|
||||
self.set_total_gain_loss()
|
||||
|
||||
def validate_rounding_loss_allowance(self):
|
||||
if not (self.rounding_loss_allowance > 0 and self.rounding_loss_allowance < 1):
|
||||
frappe.throw(_("Rounding Loss Allowance should be between 0 and 1"))
|
||||
|
||||
def set_total_gain_loss(self):
|
||||
total_gain_loss = 0
|
||||
|
||||
@@ -92,7 +97,12 @@ class ExchangeRateRevaluation(Document):
|
||||
def get_accounts_data(self):
|
||||
self.validate_mandatory()
|
||||
account_details = self.get_account_balance_from_gle(
|
||||
company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None
|
||||
company=self.company,
|
||||
posting_date=self.posting_date,
|
||||
account=None,
|
||||
party_type=None,
|
||||
party=None,
|
||||
rounding_loss_allowance=self.rounding_loss_allowance,
|
||||
)
|
||||
accounts_with_new_balance = self.calculate_new_account_balance(
|
||||
self.company, self.posting_date, account_details
|
||||
@@ -104,7 +114,9 @@ class ExchangeRateRevaluation(Document):
|
||||
return accounts_with_new_balance
|
||||
|
||||
@staticmethod
|
||||
def get_account_balance_from_gle(company, posting_date, account, party_type, party):
|
||||
def get_account_balance_from_gle(
|
||||
company, posting_date, account, party_type, party, rounding_loss_allowance
|
||||
):
|
||||
account_details = []
|
||||
|
||||
if company and posting_date:
|
||||
@@ -172,10 +184,18 @@ class ExchangeRateRevaluation(Document):
|
||||
)
|
||||
|
||||
# round off balance based on currency precision
|
||||
# and consider debit-credit difference allowance
|
||||
currency_precision = get_currency_precision()
|
||||
rounding_loss_allowance = rounding_loss_allowance or 0.05
|
||||
for acc in account_details:
|
||||
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
|
||||
if abs(acc.balance_in_account_currency) <= rounding_loss_allowance:
|
||||
acc.balance_in_account_currency = 0
|
||||
|
||||
acc.balance = flt(acc.balance, currency_precision)
|
||||
if abs(acc.balance) <= rounding_loss_allowance:
|
||||
acc.balance = 0
|
||||
|
||||
acc.zero_balance = (
|
||||
True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False
|
||||
)
|
||||
@@ -531,7 +551,9 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_details(company, posting_date, account, party_type=None, party=None):
|
||||
def get_account_details(
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance=None
|
||||
):
|
||||
if not (company and posting_date):
|
||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
||||
|
||||
@@ -549,7 +571,12 @@ def get_account_details(company, posting_date, account, party_type=None, party=N
|
||||
"account_currency": account_currency,
|
||||
}
|
||||
account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
|
||||
company=company, posting_date=posting_date, account=account, party_type=party_type, party=party
|
||||
company=company,
|
||||
posting_date=posting_date,
|
||||
account=account,
|
||||
party_type=party_type,
|
||||
party=party,
|
||||
rounding_loss_allowance=rounding_loss_allowance,
|
||||
)
|
||||
|
||||
if account_balance and (
|
||||
|
||||
@@ -940,6 +940,7 @@ class JournalEntry(AccountsController):
|
||||
blank_row.debit_in_account_currency = abs(diff)
|
||||
blank_row.debit = abs(diff)
|
||||
|
||||
self.set_total_debit_credit()
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -148,19 +148,57 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
def validate_allocated_amount(self):
|
||||
for d in self.get("references"):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
}
|
||||
)
|
||||
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
|
||||
|
||||
for d in self.get("references").copy():
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(d.reference_doctype, d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif (
|
||||
latest.outstanding_amount < latest.invoice_amount
|
||||
and d.outstanding_amount != latest.outstanding_amount
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' button to get the latest outstanding amount."
|
||||
).format(d.reference_doctype, d.reference_name)
|
||||
)
|
||||
|
||||
d.outstanding_amount = latest.outstanding_amount
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (flt(d.allocated_amount)) > 0:
|
||||
if flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
|
||||
)
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0:
|
||||
if flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
|
||||
)
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
@@ -373,7 +411,7 @@ class PaymentEntry(AccountsController):
|
||||
for k, v in no_oustanding_refs.items():
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry."
|
||||
"{} - {} now has {} as it had no outstanding amount left before submitting the Payment Entry."
|
||||
).format(
|
||||
_(k),
|
||||
frappe.bold(", ".join(d.reference_name for d in v)),
|
||||
@@ -1449,7 +1487,7 @@ def get_orders_to_be_billed(
|
||||
if voucher_type:
|
||||
doc = frappe.get_doc({"doctype": voucher_type})
|
||||
condition = ""
|
||||
if doc and hasattr(doc, "cost_center"):
|
||||
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
||||
condition = " and cost_center='%s'" % cost_center
|
||||
|
||||
orders = []
|
||||
@@ -1495,9 +1533,15 @@ def get_orders_to_be_billed(
|
||||
|
||||
order_list = []
|
||||
for d in orders:
|
||||
if not (
|
||||
flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
|
||||
and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))
|
||||
if (
|
||||
filters
|
||||
and filters.get("outstanding_amt_greater_than")
|
||||
and filters.get("outstanding_amt_less_than")
|
||||
and not (
|
||||
flt(filters.get("outstanding_amt_greater_than"))
|
||||
<= flt(d.outstanding_amount)
|
||||
<= flt(filters.get("outstanding_amt_less_than"))
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
|
||||
@@ -1013,6 +1013,30 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
employee = make_employee("test_payment_entry@salary.com", company="_Test Company")
|
||||
create_payment_entry(party_type="Employee", party=employee, save=True)
|
||||
|
||||
def test_duplicate_payment_entry_allocate_amount(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
pe_draft = get_payment_entry("Sales Invoice", si.name)
|
||||
pe_draft.insert()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pe_draft.submit)
|
||||
|
||||
def test_duplicate_payment_entry_partial_allocate_amount(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
pe_draft = get_payment_entry("Sales Invoice", si.name)
|
||||
pe_draft.insert()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.received_amount = si.total / 2
|
||||
pe.references[0].allocated_amount = si.total / 2
|
||||
pe.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pe_draft.submit)
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -6,7 +6,6 @@ import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
@@ -127,12 +126,29 @@ class PaymentReconciliation(Document):
|
||||
|
||||
return list(journal_entries)
|
||||
|
||||
def get_return_invoices(self):
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
doc = qb.DocType(voucher_type)
|
||||
self.return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(
|
||||
ConstantColumn(voucher_type).as_("voucher_type"),
|
||||
doc.name.as_("voucher_no"),
|
||||
doc.return_against,
|
||||
)
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
|
||||
self.build_qb_filter_conditions(get_return_invoices=True)
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable":
|
||||
self.common_filter_conditions.append(ple.account_type == "Receivable")
|
||||
@@ -140,19 +156,10 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions.append(ple.account_type == "Payable")
|
||||
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
||||
|
||||
# get return invoices
|
||||
doc = qb.DocType(voucher_type)
|
||||
return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
& (IfNull(doc.return_against, "") == "")
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
self.get_return_invoices()
|
||||
return_invoices = [
|
||||
x for x in self.return_invoices if x.return_against == None or x.return_against == ""
|
||||
]
|
||||
|
||||
outstanding_dr_or_cr = []
|
||||
if return_invoices:
|
||||
@@ -204,6 +211,15 @@ class PaymentReconciliation(Document):
|
||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||
)
|
||||
|
||||
cr_dr_notes = (
|
||||
[x.voucher_no for x in self.return_invoices]
|
||||
if self.party_type in ["Customer", "Supplier"]
|
||||
else []
|
||||
)
|
||||
# Filter out cr/dr notes from outstanding invoices list
|
||||
# Happens when non-standalone cr/dr notes are linked with another invoice through journal entry
|
||||
non_reconciled_invoices = [x for x in non_reconciled_invoices if x.voucher_no not in cr_dr_notes]
|
||||
|
||||
if self.invoice_limit:
|
||||
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
voucher_type="Period Closing Voucher",
|
||||
voucher_no=self.name,
|
||||
queue="long",
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, getdate
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
|
||||
class TaxWithholdingCategory(Document):
|
||||
@@ -569,7 +569,12 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
||||
tds_amount = 0
|
||||
limit_consumed = frappe.db.get_value(
|
||||
"Purchase Invoice",
|
||||
{"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1},
|
||||
{
|
||||
"supplier": ("in", parties),
|
||||
"apply_tds": 1,
|
||||
"docstatus": 1,
|
||||
"posting_date": ("between", (ldc.valid_from, ldc.valid_upto)),
|
||||
},
|
||||
"sum(tax_withholding_net_total)",
|
||||
)
|
||||
|
||||
@@ -584,10 +589,10 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
||||
|
||||
|
||||
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
|
||||
if current_amount < (certificate_limit - deducted_amount):
|
||||
if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
|
||||
return current_amount * rate / 100
|
||||
else:
|
||||
ltds_amount = certificate_limit - deducted_amount
|
||||
ltds_amount = certificate_limit - flt(deducted_amount)
|
||||
tds_amount = current_amount - ltds_amount
|
||||
|
||||
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
|
||||
@@ -598,9 +603,9 @@ def is_valid_certificate(
|
||||
):
|
||||
valid = False
|
||||
|
||||
if (
|
||||
getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)
|
||||
) and certificate_limit > deducted_amount:
|
||||
available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_amount)
|
||||
|
||||
if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
|
||||
valid = True
|
||||
|
||||
return valid
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.contacts.doctype.address.address import (
|
||||
@@ -850,7 +852,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
return company_wise_info
|
||||
|
||||
|
||||
def get_party_shipping_address(doctype, name):
|
||||
def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
||||
"""
|
||||
Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true.
|
||||
and/or `is_shipping_address = 1`.
|
||||
@@ -861,22 +863,23 @@ def get_party_shipping_address(doctype, name):
|
||||
:param name: Party name
|
||||
:return: String
|
||||
"""
|
||||
out = frappe.db.sql(
|
||||
"SELECT dl.parent "
|
||||
"from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name "
|
||||
"where "
|
||||
"dl.link_doctype=%s "
|
||||
"and dl.link_name=%s "
|
||||
"and dl.parenttype='Address' "
|
||||
"and ifnull(ta.disabled, 0) = 0 and"
|
||||
"(ta.address_type='Shipping' or ta.is_shipping_address=1) "
|
||||
"order by ta.is_shipping_address desc, ta.address_type desc limit 1",
|
||||
(doctype, name),
|
||||
shipping_addresses = frappe.get_all(
|
||||
"Address",
|
||||
filters=[
|
||||
["Dynamic Link", "link_doctype", "=", doctype],
|
||||
["Dynamic Link", "link_name", "=", name],
|
||||
["disabled", "=", 0],
|
||||
],
|
||||
or_filters=[
|
||||
["is_shipping_address", "=", 1],
|
||||
["address_type", "=", "Shipping"],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
order_by="is_shipping_address DESC",
|
||||
)
|
||||
if out:
|
||||
return out[0][0]
|
||||
else:
|
||||
return ""
|
||||
|
||||
return shipping_addresses[0] if shipping_addresses else None
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
@@ -910,31 +913,32 @@ def get_partywise_advanced_payment_amount(
|
||||
return frappe._dict(data)
|
||||
|
||||
|
||||
def get_default_contact(doctype, name):
|
||||
def get_default_contact(doctype: str, name: str) -> Optional[str]:
|
||||
"""
|
||||
Returns default contact for the given doctype and name.
|
||||
Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact.
|
||||
Returns contact name only if there is a primary contact for given doctype and name.
|
||||
|
||||
Else returns None
|
||||
|
||||
:param doctype: Party Doctype
|
||||
:param name: Party name
|
||||
:return: String
|
||||
"""
|
||||
out = frappe.db.sql(
|
||||
"""
|
||||
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact
|
||||
FROM `tabDynamic Link` dl
|
||||
INNER JOIN `tabContact` c ON c.name = dl.parent
|
||||
WHERE
|
||||
dl.link_doctype=%s AND
|
||||
dl.link_name=%s AND
|
||||
dl.parenttype = 'Contact'
|
||||
ORDER BY is_primary_contact DESC, is_billing_contact DESC
|
||||
""",
|
||||
(doctype, name),
|
||||
contacts = frappe.get_all(
|
||||
"Contact",
|
||||
filters=[
|
||||
["Dynamic Link", "link_doctype", "=", doctype],
|
||||
["Dynamic Link", "link_name", "=", name],
|
||||
],
|
||||
or_filters=[
|
||||
["is_primary_contact", "=", 1],
|
||||
["is_billing_contact", "=", 1],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
order_by="is_primary_contact DESC, is_billing_contact DESC",
|
||||
)
|
||||
if out:
|
||||
try:
|
||||
return out[0][0]
|
||||
except Exception:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
return contacts[0] if contacts else None
|
||||
|
||||
|
||||
def add_party_account(party_type, party, company, account):
|
||||
|
||||
@@ -181,6 +181,16 @@ class ReceivablePayableReport(object):
|
||||
return
|
||||
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
|
||||
# If payment is made against credit note
|
||||
# and credit note is made against a Sales Invoice
|
||||
# then consider the payment against original sales invoice.
|
||||
if ple.against_voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
if ple.against_voucher_no in self.return_entries:
|
||||
return_against = self.return_entries.get(ple.against_voucher_no)
|
||||
if return_against:
|
||||
key = (ple.against_voucher_type, return_against, ple.party)
|
||||
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
if not row:
|
||||
@@ -610,7 +620,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_return_entries(self):
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
filters = {"is_return": 1, "docstatus": 1}
|
||||
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
||||
party_field = scrub(self.filters.party_type)
|
||||
if self.filters.get(party_field):
|
||||
filters.update({party_field: self.filters.get(party_field)})
|
||||
|
||||
@@ -210,6 +210,67 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_payment_against_credit_note(self):
|
||||
"""
|
||||
Payment against credit/debit note should be considered against the parent invoice
|
||||
"""
|
||||
company = "_Test Company 2"
|
||||
customer = "_Test Customer 2"
|
||||
|
||||
si1 = make_sales_invoice()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
|
||||
pe.paid_from = "Debtors - _TC2"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
cr_note = make_credit_note(si1.name)
|
||||
|
||||
si2 = make_sales_invoice()
|
||||
|
||||
# manually link cr_note with si2 using journal entry
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = company
|
||||
je.voucher_type = "Credit Note"
|
||||
je.posting_date = today()
|
||||
|
||||
debit_account = "Debtors - _TC2"
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"debit": 100,
|
||||
"debit_in_account_currency": 100,
|
||||
"reference_type": cr_note.doctype,
|
||||
"reference_name": cr_note.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
}
|
||||
credit_entry = {
|
||||
"account": debit_account,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"credit": 100,
|
||||
"credit_in_account_currency": 100,
|
||||
"reference_type": si2.doctype,
|
||||
"reference_name": si2.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
}
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
je.append("accounts", credit_entry)
|
||||
je = je.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
report = execute(filters)
|
||||
self.assertEqual(report[1], [])
|
||||
|
||||
|
||||
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
@@ -256,7 +317,7 @@ def make_payment(docname):
|
||||
|
||||
|
||||
def make_credit_note(docname):
|
||||
create_sales_invoice(
|
||||
credit_note = create_sales_invoice(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
currency="EUR",
|
||||
@@ -269,3 +330,5 @@ def make_credit_note(docname):
|
||||
is_return=1,
|
||||
return_against=docname,
|
||||
)
|
||||
|
||||
return credit_note
|
||||
|
||||
@@ -399,8 +399,9 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
|
||||
`tabSales Invoice`.unrealized_profit_loss_account,
|
||||
`tabSales Invoice`.is_internal_customer,
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
|
||||
`tabSales Invoice Item`.project,
|
||||
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
||||
|
||||
@@ -917,6 +917,9 @@ class AccountsController(TransactionBase):
|
||||
|
||||
return is_inclusive
|
||||
|
||||
def should_show_taxes_as_table_in_print(self):
|
||||
return cint(frappe.db.get_single_value("Accounts Settings", "show_taxes_as_table_in_print"))
|
||||
|
||||
def validate_advance_entries(self):
|
||||
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
|
||||
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
@@ -30,10 +30,16 @@ def set_print_templates_for_taxes(doc, settings):
|
||||
doc.print_templates.update(
|
||||
{
|
||||
"total": "templates/print_formats/includes/total.html",
|
||||
"taxes": "templates/print_formats/includes/taxes.html",
|
||||
}
|
||||
)
|
||||
|
||||
if not doc.should_show_taxes_as_table_in_print():
|
||||
doc.print_templates.update(
|
||||
{
|
||||
"taxes": "templates/print_formats/includes/taxes.html",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def format_columns(display_columns, compact_fields):
|
||||
compact_fields = compact_fields + ["image", "item_code", "item_name"]
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.contacts.address_and_contact import (
|
||||
delete_contact_and_address,
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
|
||||
@@ -43,9 +46,8 @@ class Lead(SellingController, CRMNote):
|
||||
self.update_prospect()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
|
||||
|
||||
self.unlink_dynamic_links()
|
||||
frappe.db.set_value("Issue", {"lead": self.name}, "lead", None)
|
||||
delete_contact_and_address(self.doctype, self.name)
|
||||
self.remove_link_from_prospect()
|
||||
|
||||
def set_full_name(self):
|
||||
@@ -122,27 +124,6 @@ class Lead(SellingController, CRMNote):
|
||||
)
|
||||
lead_row.db_update()
|
||||
|
||||
def unlink_dynamic_links(self):
|
||||
links = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"link_doctype": self.doctype, "link_name": self.name},
|
||||
fields=["parent", "parenttype"],
|
||||
)
|
||||
|
||||
for link in links:
|
||||
linked_doc = frappe.get_doc(link["parenttype"], link["parent"])
|
||||
|
||||
if len(linked_doc.get("links")) == 1:
|
||||
linked_doc.delete(ignore_permissions=True)
|
||||
else:
|
||||
to_remove = None
|
||||
for d in linked_doc.get("links"):
|
||||
if d.link_doctype == self.doctype and d.link_name == self.name:
|
||||
to_remove = d
|
||||
if to_remove:
|
||||
linked_doc.remove(to_remove)
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
def remove_link_from_prospect(self):
|
||||
prospects = self.get_linked_prospects()
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.contacts.address_and_contact import (
|
||||
delete_contact_and_address,
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
@@ -16,7 +19,7 @@ class Prospect(CRMNote):
|
||||
self.link_with_lead_contact_and_address()
|
||||
|
||||
def on_trash(self):
|
||||
self.unlink_dynamic_links()
|
||||
delete_contact_and_address(self.doctype, self.name)
|
||||
|
||||
def after_insert(self):
|
||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||
@@ -54,27 +57,6 @@ class Prospect(CRMNote):
|
||||
linked_doc.append("links", {"link_doctype": self.doctype, "link_name": self.name})
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
def unlink_dynamic_links(self):
|
||||
links = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"link_doctype": self.doctype, "link_name": self.name},
|
||||
fields=["parent", "parenttype"],
|
||||
)
|
||||
|
||||
for link in links:
|
||||
linked_doc = frappe.get_doc(link["parenttype"], link["parent"])
|
||||
|
||||
if len(linked_doc.get("links")) == 1:
|
||||
linked_doc.delete(ignore_permissions=True)
|
||||
else:
|
||||
to_remove = None
|
||||
for d in linked_doc.get("links"):
|
||||
if d.link_doctype == self.doctype and d.link_name == self.name:
|
||||
to_remove = d
|
||||
if to_remove:
|
||||
linked_doc.remove(to_remove)
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_customer(source_name, target_doc=None):
|
||||
|
||||
@@ -78,9 +78,10 @@ erpnext.ProductList = class {
|
||||
let title_html = `<div style="display: flex; margin-left: -15px;">`;
|
||||
title_html += `
|
||||
<div class="col-8" style="margin-right: -15px;">
|
||||
<a class="" href="/${ item.route || '#' }"
|
||||
style="color: var(--gray-800); font-weight: 500;">
|
||||
<a href="/${ item.route || '#' }">
|
||||
<div class="product-title">
|
||||
${ title }
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
@@ -201,4 +202,4 @@ erpnext.ProductList = class {
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
@@ -88,12 +88,14 @@ class BOMUpdateLog(Document):
|
||||
boms=boms,
|
||||
timeout=40000,
|
||||
now=frappe.flags.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
else:
|
||||
frappe.enqueue(
|
||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
||||
update_doc=self,
|
||||
now=frappe.flags.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ frappe.ui.form.on('Job Card', {
|
||||
// and if stock mvt for WIP is required
|
||||
if (frm.doc.work_order) {
|
||||
frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => {
|
||||
if (result.skip_transfer === 1 || result.status == 'In Process' || frm.doc.transferred_qty > 0) {
|
||||
if (result.skip_transfer === 1 || result.status == 'In Process' || frm.doc.transferred_qty > 0 || !frm.doc.items.length) {
|
||||
frm.trigger("prepare_timer_buttons");
|
||||
}
|
||||
});
|
||||
@@ -411,6 +411,16 @@ frappe.ui.form.on('Job Card', {
|
||||
}
|
||||
});
|
||||
|
||||
if (frm.doc.total_completed_qty && frm.doc.for_quantity > frm.doc.total_completed_qty) {
|
||||
let flt_precision = precision('for_quantity', frm.doc);
|
||||
let process_loss_qty = (
|
||||
flt(frm.doc.for_quantity, flt_precision)
|
||||
- flt(frm.doc.total_completed_qty, flt_precision)
|
||||
);
|
||||
|
||||
frm.set_value('process_loss_qty', process_loss_qty);
|
||||
}
|
||||
|
||||
refresh_field("total_completed_qty");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"time_logs",
|
||||
"section_break_13",
|
||||
"total_completed_qty",
|
||||
"process_loss_qty",
|
||||
"column_break_15",
|
||||
"total_time_in_mins",
|
||||
"section_break_8",
|
||||
@@ -435,11 +436,17 @@
|
||||
"fieldname": "expected_end_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Expected End Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Process Loss Qty",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-23 09:56:43.826602",
|
||||
"modified": "2023-06-09 12:04:55.534264",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
@@ -497,4 +504,4 @@
|
||||
"states": [],
|
||||
"title_field": "operation",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ class JobCard(Document):
|
||||
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
|
||||
|
||||
for row in self.sub_operations:
|
||||
self.total_completed_qty += row.completed_qty
|
||||
self.c += row.completed_qty
|
||||
|
||||
def get_overlap_for(self, args, check_next_available_slot=False):
|
||||
production_capacity = 1
|
||||
@@ -451,6 +451,9 @@ class JobCard(Document):
|
||||
},
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.set_process_loss()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_transfer_qty()
|
||||
self.validate_job_card()
|
||||
@@ -487,19 +490,35 @@ class JobCard(Document):
|
||||
)
|
||||
)
|
||||
|
||||
if self.for_quantity and self.total_completed_qty != self.for_quantity:
|
||||
precision = self.precision("total_completed_qty")
|
||||
total_completed_qty = flt(
|
||||
flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision)
|
||||
)
|
||||
|
||||
if self.for_quantity and flt(total_completed_qty, precision) != flt(
|
||||
self.for_quantity, precision
|
||||
):
|
||||
total_completed_qty = bold(_("Total Completed Qty"))
|
||||
qty_to_manufacture = bold(_("Qty to Manufacture"))
|
||||
|
||||
frappe.throw(
|
||||
_("The {0} ({1}) must be equal to {2} ({3})").format(
|
||||
total_completed_qty,
|
||||
bold(self.total_completed_qty),
|
||||
bold(flt(total_completed_qty, precision)),
|
||||
qty_to_manufacture,
|
||||
bold(self.for_quantity),
|
||||
)
|
||||
)
|
||||
|
||||
def set_process_loss(self):
|
||||
precision = self.precision("total_completed_qty")
|
||||
|
||||
self.process_loss_qty = 0.0
|
||||
if self.total_completed_qty and self.for_quantity > self.total_completed_qty:
|
||||
self.process_loss_qty = flt(self.for_quantity, precision) - flt(
|
||||
self.total_completed_qty, precision
|
||||
)
|
||||
|
||||
def update_work_order(self):
|
||||
if not self.work_order:
|
||||
return
|
||||
@@ -511,7 +530,7 @@ class JobCard(Document):
|
||||
):
|
||||
return
|
||||
|
||||
for_quantity, time_in_mins = 0, 0
|
||||
for_quantity, time_in_mins, process_loss_qty = 0, 0, 0
|
||||
from_time_list, to_time_list = [], []
|
||||
|
||||
field = "operation_id"
|
||||
@@ -519,6 +538,7 @@ class JobCard(Document):
|
||||
if data and len(data) > 0:
|
||||
for_quantity = flt(data[0].completed_qty)
|
||||
time_in_mins = flt(data[0].time_in_mins)
|
||||
process_loss_qty = flt(data[0].process_loss_qty)
|
||||
|
||||
wo = frappe.get_doc("Work Order", self.work_order)
|
||||
|
||||
@@ -526,8 +546,8 @@ class JobCard(Document):
|
||||
self.update_corrective_in_work_order(wo)
|
||||
|
||||
elif self.operation_id:
|
||||
self.validate_produced_quantity(for_quantity, wo)
|
||||
self.update_work_order_data(for_quantity, time_in_mins, wo)
|
||||
self.validate_produced_quantity(for_quantity, process_loss_qty, wo)
|
||||
self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo)
|
||||
|
||||
def update_corrective_in_work_order(self, wo):
|
||||
wo.corrective_operation_cost = 0.0
|
||||
@@ -542,11 +562,11 @@ class JobCard(Document):
|
||||
wo.flags.ignore_validate_update_after_submit = True
|
||||
wo.save()
|
||||
|
||||
def validate_produced_quantity(self, for_quantity, wo):
|
||||
def validate_produced_quantity(self, for_quantity, process_loss_qty, wo):
|
||||
if self.docstatus < 2:
|
||||
return
|
||||
|
||||
if wo.produced_qty > for_quantity:
|
||||
if wo.produced_qty > for_quantity + process_loss_qty:
|
||||
first_part_msg = _(
|
||||
"The {0} {1} is used to calculate the valuation cost for the finished good {2}."
|
||||
).format(
|
||||
@@ -561,7 +581,7 @@ class JobCard(Document):
|
||||
_("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error")
|
||||
)
|
||||
|
||||
def update_work_order_data(self, for_quantity, time_in_mins, wo):
|
||||
def update_work_order_data(self, for_quantity, process_loss_qty, time_in_mins, wo):
|
||||
workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate")
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||
@@ -582,6 +602,7 @@ class JobCard(Document):
|
||||
for data in wo.operations:
|
||||
if data.get("name") == self.operation_id:
|
||||
data.completed_qty = for_quantity
|
||||
data.process_loss_qty = process_loss_qty
|
||||
data.actual_operation_time = time_in_mins
|
||||
data.actual_start_time = time_data[0].start_time if time_data else None
|
||||
data.actual_end_time = time_data[0].end_time if time_data else None
|
||||
@@ -599,7 +620,11 @@ class JobCard(Document):
|
||||
def get_current_operation_data(self):
|
||||
return frappe.get_all(
|
||||
"Job Card",
|
||||
fields=["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
|
||||
fields=[
|
||||
"sum(total_time_in_mins) as time_in_mins",
|
||||
"sum(total_completed_qty) as completed_qty",
|
||||
"sum(process_loss_qty) as process_loss_qty",
|
||||
],
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"work_order": self.work_order,
|
||||
@@ -777,7 +802,7 @@ class JobCard(Document):
|
||||
|
||||
data = frappe.get_all(
|
||||
"Work Order Operation",
|
||||
fields=["operation", "status", "completed_qty"],
|
||||
fields=["operation", "status", "completed_qty", "sequence_id"],
|
||||
filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)},
|
||||
order_by="sequence_id, idx",
|
||||
)
|
||||
@@ -795,6 +820,16 @@ class JobCard(Document):
|
||||
OperationSequenceError,
|
||||
)
|
||||
|
||||
if row.completed_qty < current_operation_qty:
|
||||
msg = f"""The completed quantity {bold(current_operation_qty)}
|
||||
of an operation {bold(self.operation)} cannot be greater
|
||||
than the completed quantity {bold(row.completed_qty)}
|
||||
of a previous operation
|
||||
{bold(row.operation)}.
|
||||
"""
|
||||
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def validate_work_order(self):
|
||||
if self.is_work_order_closed():
|
||||
frappe.throw(_("You can't make any changes to Job Card since Work Order is closed."))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from typing import Literal
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import random_string
|
||||
from frappe.utils.data import add_to_date, now, today
|
||||
@@ -469,6 +470,119 @@ class TestJobCard(FrappeTestCase):
|
||||
self.assertEqual(ste.from_bom, 1.0)
|
||||
self.assertEqual(ste.bom_no, work_order.bom_no)
|
||||
|
||||
def test_job_card_proccess_qty_and_completed_qty(self):
|
||||
from erpnext.manufacturing.doctype.routing.test_routing import (
|
||||
create_routing,
|
||||
setup_bom,
|
||||
setup_operations,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
operations = [
|
||||
{"operation": "Test Operation A1", "workstation": "Test Workstation A", "time_in_mins": 30},
|
||||
{"operation": "Test Operation B1", "workstation": "Test Workstation A", "time_in_mins": 20},
|
||||
]
|
||||
|
||||
make_test_records("UOM")
|
||||
|
||||
warehouse = create_warehouse("Test Warehouse 123 for Job Card")
|
||||
|
||||
setup_operations(operations)
|
||||
|
||||
item_code = "Test Job Card Process Qty Item"
|
||||
for item in [item_code, item_code + "RM 1", item_code + "RM 2"]:
|
||||
if not frappe.db.exists("Item", item):
|
||||
make_item(
|
||||
item,
|
||||
{
|
||||
"item_name": item,
|
||||
"stock_uom": "Nos",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
|
||||
bom_doc = setup_bom(
|
||||
item_code=item_code,
|
||||
routing=routing_doc.name,
|
||||
raw_materials=[item_code + "RM 1", item_code + "RM 2"],
|
||||
source_warehouse=warehouse,
|
||||
)
|
||||
|
||||
for row in bom_doc.items:
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target=row.source_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
wo_doc = make_wo_order_test_record(
|
||||
production_item=item_code,
|
||||
bom_no=bom_doc.name,
|
||||
skip_transfer=1,
|
||||
wip_warehouse=warehouse,
|
||||
source_warehouse=warehouse,
|
||||
)
|
||||
|
||||
for row in routing_doc.operations:
|
||||
self.assertEqual(row.sequence_id, row.idx)
|
||||
|
||||
first_job_card = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name, "sequence_id": 1},
|
||||
fields=["name"],
|
||||
order_by="sequence_id",
|
||||
limit=1,
|
||||
)[0].name
|
||||
|
||||
jc = frappe.get_doc("Job Card", first_job_card)
|
||||
jc.time_logs[0].completed_qty = 8
|
||||
jc.save()
|
||||
jc.submit()
|
||||
|
||||
self.assertEqual(jc.process_loss_qty, 2)
|
||||
self.assertEqual(jc.for_quantity, 10)
|
||||
|
||||
second_job_card = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name, "sequence_id": 2},
|
||||
fields=["name"],
|
||||
order_by="sequence_id",
|
||||
limit=1,
|
||||
)[0].name
|
||||
|
||||
jc2 = frappe.get_doc("Job Card", second_job_card)
|
||||
jc2.time_logs[0].completed_qty = 10
|
||||
|
||||
self.assertRaises(frappe.ValidationError, jc2.save)
|
||||
|
||||
jc2.load_from_db()
|
||||
jc2.time_logs[0].completed_qty = 8
|
||||
jc2.save()
|
||||
jc2.submit()
|
||||
|
||||
self.assertEqual(jc2.for_quantity, 10)
|
||||
self.assertEqual(jc2.process_loss_qty, 2)
|
||||
|
||||
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 10))
|
||||
s.submit()
|
||||
|
||||
self.assertEqual(s.process_loss_qty, 2)
|
||||
|
||||
wo_doc.reload()
|
||||
for row in wo_doc.operations:
|
||||
self.assertEqual(row.completed_qty, 8)
|
||||
self.assertEqual(row.process_loss_qty, 2)
|
||||
|
||||
self.assertEqual(wo_doc.produced_qty, 8)
|
||||
self.assertEqual(wo_doc.process_loss_qty, 2)
|
||||
self.assertEqual(wo_doc.status, "Completed")
|
||||
|
||||
|
||||
def create_bom_with_multiple_operations():
|
||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||
|
||||
@@ -141,6 +141,7 @@ def setup_bom(**args):
|
||||
routing=args.routing,
|
||||
with_operations=1,
|
||||
currency=args.currency,
|
||||
source_warehouse=args.source_warehouse,
|
||||
)
|
||||
else:
|
||||
bom_doc = frappe.get_doc("BOM", name)
|
||||
|
||||
@@ -891,7 +891,7 @@ class TestWorkOrder(FrappeTestCase):
|
||||
self.assertEqual(se.process_loss_qty, 1)
|
||||
|
||||
wo.load_from_db()
|
||||
self.assertEqual(wo.status, "In Process")
|
||||
self.assertEqual(wo.status, "Completed")
|
||||
|
||||
@timeout(seconds=60)
|
||||
def test_job_card_scrap_item(self):
|
||||
|
||||
@@ -139,7 +139,7 @@ frappe.ui.form.on("Work Order", {
|
||||
}
|
||||
|
||||
if (frm.doc.status != "Closed") {
|
||||
if (frm.doc.docstatus === 1
|
||||
if (frm.doc.docstatus === 1 && frm.doc.status !== "Completed"
|
||||
&& frm.doc.operations && frm.doc.operations.length) {
|
||||
|
||||
const not_completed = frm.doc.operations.filter(d => {
|
||||
@@ -256,6 +256,12 @@ frappe.ui.form.on("Work Order", {
|
||||
label: __('Batch Size'),
|
||||
read_only: 1
|
||||
},
|
||||
{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'sequence_id',
|
||||
label: __('Sequence Id'),
|
||||
read_only: 1
|
||||
},
|
||||
],
|
||||
data: operations_data,
|
||||
in_place_edit: true,
|
||||
@@ -280,8 +286,8 @@ frappe.ui.form.on("Work Order", {
|
||||
|
||||
var pending_qty = 0;
|
||||
frm.doc.operations.forEach(data => {
|
||||
if(data.completed_qty != frm.doc.qty) {
|
||||
pending_qty = frm.doc.qty - flt(data.completed_qty);
|
||||
if(data.completed_qty + data.process_loss_qty != frm.doc.qty) {
|
||||
pending_qty = frm.doc.qty - flt(data.completed_qty) - flt(data.process_loss_qty);
|
||||
|
||||
if (pending_qty) {
|
||||
dialog.fields_dict.operations.df.data.push({
|
||||
@@ -290,7 +296,8 @@ frappe.ui.form.on("Work Order", {
|
||||
'workstation': data.workstation,
|
||||
'batch_size': data.batch_size,
|
||||
'qty': pending_qty,
|
||||
'pending_qty': pending_qty
|
||||
'pending_qty': pending_qty,
|
||||
'sequence_id': data.sequence_id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
"required_items_section",
|
||||
"materials_and_operations_tab",
|
||||
"operations_section",
|
||||
"operations",
|
||||
"transfer_material_against",
|
||||
"operations",
|
||||
"time",
|
||||
"planned_start_date",
|
||||
"planned_end_date",
|
||||
@@ -331,7 +331,6 @@
|
||||
"label": "Expected Delivery Date"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "operations_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Operations",
|
||||
@@ -599,7 +598,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-06 12:35:12.149827",
|
||||
"modified": "2023-06-09 13:20:09.154362",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -249,7 +249,9 @@ class WorkOrder(Document):
|
||||
status = "Not Started"
|
||||
if flt(self.material_transferred_for_manufacturing) > 0:
|
||||
status = "In Process"
|
||||
if flt(self.produced_qty) >= flt(self.qty):
|
||||
|
||||
total_qty = flt(self.produced_qty) + flt(self.process_loss_qty)
|
||||
if flt(total_qty) >= flt(self.qty):
|
||||
status = "Completed"
|
||||
else:
|
||||
status = "Cancelled"
|
||||
@@ -736,13 +738,15 @@ class WorkOrder(Document):
|
||||
max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty))
|
||||
|
||||
for d in self.get("operations"):
|
||||
if not d.completed_qty:
|
||||
precision = d.precision("completed_qty")
|
||||
qty = flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision)
|
||||
if not qty:
|
||||
d.status = "Pending"
|
||||
elif flt(d.completed_qty) < flt(self.qty):
|
||||
elif flt(qty) < flt(self.qty):
|
||||
d.status = "Work in Progress"
|
||||
elif flt(d.completed_qty) == flt(self.qty):
|
||||
elif flt(qty) == flt(self.qty):
|
||||
d.status = "Completed"
|
||||
elif flt(d.completed_qty) <= max_allowed_qty_for_wo:
|
||||
elif flt(qty) <= max_allowed_qty_for_wo:
|
||||
d.status = "Completed"
|
||||
else:
|
||||
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
"actions": [],
|
||||
"creation": "2014-10-16 14:35:41.950175",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"details",
|
||||
"operation",
|
||||
"status",
|
||||
"completed_qty",
|
||||
"process_loss_qty",
|
||||
"column_break_4",
|
||||
"bom",
|
||||
"workstation_type",
|
||||
@@ -36,6 +38,7 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "operation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -46,6 +49,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "bom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -62,7 +66,7 @@
|
||||
"oldfieldtype": "Text"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"columns": 2,
|
||||
"description": "Operation completed for how many finished goods?",
|
||||
"fieldname": "completed_qty",
|
||||
"fieldtype": "Float",
|
||||
@@ -80,6 +84,7 @@
|
||||
"options": "Pending\nWork in Progress\nCompleted"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "workstation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -115,7 +120,7 @@
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Operation Time",
|
||||
"label": "Time",
|
||||
"oldfieldname": "time_in_mins",
|
||||
"oldfieldtype": "Currency",
|
||||
"reqd": 1
|
||||
@@ -203,12 +208,21 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Workstation Type",
|
||||
"options": "Workstation Type"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Process Loss Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-09 01:37:56.563068",
|
||||
"modified": "2023-06-09 14:03:01.612909",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Operation",
|
||||
|
||||
@@ -805,11 +805,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
);
|
||||
}
|
||||
|
||||
this.frm.doc.payments.find(pay => {
|
||||
if (pay.default) {
|
||||
pay.amount = total_amount_to_pay;
|
||||
}
|
||||
});
|
||||
if(!this.frm.doc.is_return){
|
||||
this.frm.doc.payments.find(payment => {
|
||||
if (payment.default) {
|
||||
payment.amount = total_amount_to_pay;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.frm.refresh_fields();
|
||||
}
|
||||
|
||||
@@ -299,6 +299,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
)
|
||||
|
||||
target.flags.ignore_permissions = ignore_permissions
|
||||
target.delivery_date = nowdate()
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
@@ -306,6 +307,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0)
|
||||
target.qty = balance_qty if balance_qty > 0 else 0
|
||||
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
|
||||
target.delivery_date = nowdate()
|
||||
|
||||
if obj.against_blanket_order:
|
||||
target.against_blanket_order = obj.against_blanket_order
|
||||
|
||||
@@ -60,9 +60,9 @@ class TestQuotation(FrappeTestCase):
|
||||
sales_order = make_sales_order(quotation.name)
|
||||
sales_order.currency = "USD"
|
||||
sales_order.conversion_rate = 20.0
|
||||
sales_order.delivery_date = "2019-01-01"
|
||||
sales_order.naming_series = "_T-Quotation-"
|
||||
sales_order.transaction_date = nowdate()
|
||||
sales_order.delivery_date = nowdate()
|
||||
sales_order.insert()
|
||||
|
||||
self.assertEqual(sales_order.currency, "USD")
|
||||
@@ -644,8 +644,6 @@ def make_quotation(**args):
|
||||
},
|
||||
)
|
||||
|
||||
qo.delivery_date = add_days(qo.transaction_date, 10)
|
||||
|
||||
if not args.do_not_save:
|
||||
qo.insert()
|
||||
if not args.do_not_submit:
|
||||
|
||||
@@ -158,7 +158,8 @@ class SalesOrder(SellingController):
|
||||
frappe.msgprint(
|
||||
_("Expected Delivery Date should be after Sales Order Date"),
|
||||
indicator="orange",
|
||||
title=_("Warning"),
|
||||
title=_("Invalid Delivery Date"),
|
||||
raise_exception=True,
|
||||
)
|
||||
else:
|
||||
frappe.throw(_("Please enter Delivery Date"))
|
||||
@@ -217,6 +218,7 @@ class SalesOrder(SellingController):
|
||||
frappe.throw(_("Quotation {0} is cancelled").format(quotation))
|
||||
|
||||
doc.set_status(update=True)
|
||||
doc.update_opportunity("Converted" if flag == "submit" else "Quotation")
|
||||
|
||||
def validate_drop_ship(self):
|
||||
for d in self.get("items"):
|
||||
|
||||
@@ -215,13 +215,39 @@ def hide_workspaces():
|
||||
|
||||
|
||||
def create_default_role_profiles():
|
||||
for module in ["Accounts", "Stock", "Manufacturing"]:
|
||||
create_role_profile(module)
|
||||
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
|
||||
role_profile = frappe.new_doc("Role Profile")
|
||||
role_profile.role_profile = role_profile_name
|
||||
for role in roles:
|
||||
role_profile.append("roles", {"role": role})
|
||||
|
||||
role_profile.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def create_role_profile(module):
|
||||
role_profile = frappe.new_doc("Role Profile")
|
||||
role_profile.role_profile = _("{0} User").format(module)
|
||||
role_profile.append("roles", {"role": module + " User"})
|
||||
role_profile.append("roles", {"role": module + " Manager"})
|
||||
role_profile.insert()
|
||||
DEFAULT_ROLE_PROFILES = {
|
||||
"Inventory": [
|
||||
"Stock User",
|
||||
"Stock Manager",
|
||||
"Item Manager",
|
||||
],
|
||||
"Manufacturing": [
|
||||
"Stock User",
|
||||
"Manufacturing User",
|
||||
"Manufacturing Manager",
|
||||
],
|
||||
"Accounts": [
|
||||
"Accounts User",
|
||||
"Accounts Manager",
|
||||
],
|
||||
"Sales": [
|
||||
"Sales User",
|
||||
"Stock User",
|
||||
"Sales Manager",
|
||||
],
|
||||
"Purchase": [
|
||||
"Item Manager",
|
||||
"Stock User",
|
||||
"Purchase User",
|
||||
"Purchase Manager",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class ClosingStockBalance(Document):
|
||||
|
||||
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
|
||||
if self.get(fieldname):
|
||||
query = query.where(table.get(fieldname) == self.get(fieldname))
|
||||
query = query.where(table[fieldname] == self.get(fieldname))
|
||||
|
||||
query = query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -714,6 +714,7 @@ class Item(Document):
|
||||
template=self,
|
||||
now=frappe.flags.in_test,
|
||||
timeout=600,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
def validate_has_variants(self):
|
||||
|
||||
@@ -677,6 +677,21 @@ frappe.ui.form.on('Stock Entry', {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
process_loss_qty(frm) {
|
||||
if (frm.doc.process_loss_qty) {
|
||||
frm.doc.process_loss_percentage = flt(frm.doc.process_loss_qty / frm.doc.fg_completed_qty * 100, precision("process_loss_qty", frm.doc));
|
||||
refresh_field("process_loss_percentage");
|
||||
}
|
||||
},
|
||||
|
||||
process_loss_percentage(frm) {
|
||||
debugger
|
||||
if (frm.doc.process_loss_percentage) {
|
||||
frm.doc.process_loss_qty = flt((frm.doc.fg_completed_qty * frm.doc.process_loss_percentage) / 100 , precision("process_loss_qty", frm.doc));
|
||||
refresh_field("process_loss_qty");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on('Stock Entry Detail', {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"column_break_eaoa",
|
||||
"set_posting_time",
|
||||
"inspection_required",
|
||||
"apply_putaway_rule",
|
||||
@@ -640,16 +641,16 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval: doc.fg_completed_qty > 0 && in_list([\"Manufacture\", \"Repack\"], doc.purpose)",
|
||||
"fieldname": "section_break_7qsm",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Process Loss"
|
||||
},
|
||||
{
|
||||
"depends_on": "process_loss_percentage",
|
||||
"depends_on": "eval: doc.fg_completed_qty > 0 && in_list([\"Manufacture\", \"Repack\"], doc.purpose)",
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Process Loss Qty",
|
||||
"read_only": 1
|
||||
"label": "Process Loss Qty"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_e92r",
|
||||
@@ -657,8 +658,6 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.from_bom && doc.fg_completed_qty",
|
||||
"fetch_from": "bom_no.process_loss_percentage",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "process_loss_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "% Process Loss"
|
||||
@@ -667,6 +666,10 @@
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_eaoa",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -674,7 +677,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-06 12:42:56.673180",
|
||||
"modified": "2023-06-09 15:46:28.418339",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry",
|
||||
|
||||
@@ -455,13 +455,16 @@ class StockEntry(StockController):
|
||||
if self.purpose == "Manufacture" and self.work_order:
|
||||
for d in self.items:
|
||||
if d.is_finished_item:
|
||||
if self.process_loss_qty:
|
||||
d.qty = self.fg_completed_qty - self.process_loss_qty
|
||||
|
||||
item_wise_qty.setdefault(d.item_code, []).append(d.qty)
|
||||
|
||||
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
||||
for item_code, qty_list in item_wise_qty.items():
|
||||
total = flt(sum(qty_list), precision)
|
||||
|
||||
if (self.fg_completed_qty - total) > 0:
|
||||
if (self.fg_completed_qty - total) > 0 and not self.process_loss_qty:
|
||||
self.process_loss_qty = flt(self.fg_completed_qty - total, precision)
|
||||
self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty)
|
||||
|
||||
@@ -591,7 +594,9 @@ class StockEntry(StockController):
|
||||
|
||||
for d in prod_order.get("operations"):
|
||||
total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty)
|
||||
completed_qty = d.completed_qty + (allowance_percentage / 100 * d.completed_qty)
|
||||
completed_qty = (
|
||||
d.completed_qty + d.process_loss_qty + (allowance_percentage / 100 * d.completed_qty)
|
||||
)
|
||||
if total_completed_qty > flt(completed_qty):
|
||||
job_card = frappe.db.get_value("Job Card", {"operation_id": d.name}, "name")
|
||||
if not job_card:
|
||||
@@ -1573,16 +1578,36 @@ class StockEntry(StockController):
|
||||
if self.purpose not in ("Manufacture", "Repack"):
|
||||
return
|
||||
|
||||
self.process_loss_qty = 0.0
|
||||
if not self.process_loss_percentage:
|
||||
precision = self.precision("process_loss_qty")
|
||||
if self.work_order:
|
||||
data = frappe.get_all(
|
||||
"Work Order Operation",
|
||||
filters={"parent": self.work_order},
|
||||
fields=["max(process_loss_qty) as process_loss_qty"],
|
||||
)
|
||||
|
||||
if data and data[0].process_loss_qty is not None:
|
||||
process_loss_qty = data[0].process_loss_qty
|
||||
if flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision):
|
||||
self.process_loss_qty = flt(process_loss_qty, precision)
|
||||
|
||||
frappe.msgprint(
|
||||
_("The Process Loss Qty has reset as per job cards Process Loss Qty"), alert=True
|
||||
)
|
||||
|
||||
if not self.process_loss_percentage and not self.process_loss_qty:
|
||||
self.process_loss_percentage = frappe.get_cached_value(
|
||||
"BOM", self.bom_no, "process_loss_percentage"
|
||||
)
|
||||
|
||||
if self.process_loss_percentage:
|
||||
if self.process_loss_percentage and not self.process_loss_qty:
|
||||
self.process_loss_qty = flt(
|
||||
(flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100
|
||||
)
|
||||
elif self.process_loss_qty and not self.process_loss_percentage:
|
||||
self.process_loss_percentage = flt(
|
||||
(flt(self.process_loss_qty) / flt(self.fg_completed_qty)) * 100
|
||||
)
|
||||
|
||||
def set_work_order_details(self):
|
||||
if not getattr(self, "pro_doc", None):
|
||||
|
||||
@@ -93,6 +93,7 @@ class StockSettings(Document):
|
||||
frappe.enqueue(
|
||||
"erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions",
|
||||
now=frappe.flags.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
def validate_pending_reposts(self):
|
||||
|
||||
@@ -96,14 +96,14 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D
|
||||
range1 = range2 = range3 = above_range3 = 0.0
|
||||
|
||||
for item in fifo_queue:
|
||||
age = date_diff(to_date, item[1])
|
||||
age = flt(date_diff(to_date, item[1]))
|
||||
qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0
|
||||
|
||||
if age <= filters.range1:
|
||||
if age <= flt(filters.range1):
|
||||
range1 = flt(range1 + qty, precision)
|
||||
elif age <= filters.range2:
|
||||
elif age <= flt(filters.range2):
|
||||
range2 = flt(range2 + qty, precision)
|
||||
elif age <= filters.range3:
|
||||
elif age <= flt(filters.range3):
|
||||
range3 = flt(range3 + qty, precision)
|
||||
else:
|
||||
above_range3 = flt(above_range3 + qty, precision)
|
||||
|
||||
@@ -803,7 +803,7 @@ class update_entries_after(object):
|
||||
|
||||
for item in sr.items:
|
||||
# Skip for Serial and Batch Items
|
||||
if item.serial_no or item.batch_no:
|
||||
if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
|
||||
continue
|
||||
|
||||
previous_sle = get_previous_sle(
|
||||
|
||||
@@ -7638,20 +7638,19 @@ Restaurant Order Entry Item,Restaurantbestellzugangsposten,
|
||||
Served,Serviert,
|
||||
Restaurant Reservation,Restaurant Reservierung,
|
||||
Waitlisted,Auf der Warteliste,
|
||||
No Show,Keine Show,
|
||||
No of People,Nein von Menschen,
|
||||
No Show,Nicht angetreten,
|
||||
No of People,Anzahl von Personen,
|
||||
Reservation Time,Reservierungszeit,
|
||||
Reservation End Time,Reservierungsendzeit,
|
||||
No of Seats,Anzahl der Sitze,
|
||||
Minimum Seating,Mindestbestuhlung,
|
||||
"Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ","Verkaufskampagne verfolgen: Leads, Angebote, Aufträge usw. von Kampagnen beobachten um die Kapitalverzinsung (RoI) zu messen.",
|
||||
SAL-CAM-.YYYY.-,SAL-CAM-.YYYY.-,
|
||||
Campaign Schedules,Kampagnenpläne,
|
||||
Buyer of Goods and Services.,Käufer von Waren und Dienstleistungen.,
|
||||
CUST-.YYYY.-,CUST-.YYYY.-,
|
||||
Default Company Bank Account,Standard-Bankkonto des Unternehmens,
|
||||
From Lead,Aus Lead,
|
||||
Account Manager,Buchhalter,
|
||||
Account Manager,Kundenberater,
|
||||
Accounts Manager,Buchhalter,
|
||||
Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag,
|
||||
Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein,
|
||||
Default Price List,Standardpreisliste,
|
||||
@@ -7692,7 +7691,6 @@ Quantity of Items,Anzahl der Artikel,
|
||||
"Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have ""Is Stock Item"" as ""No"" and ""Is Sales Item"" as ""Yes"".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials","Fassen Sie eine Gruppe von Artikeln zu einem neuen Artikel zusammen. Dies ist nützlich, wenn Sie bestimmte Artikel zu einem Paket bündeln und einen Bestand an Artikel-Bündeln erhalten und nicht einen Bestand der einzelnen Artikel. Das Artikel-Bündel erhält für das Attribut ""Ist Lagerartikel"" den Wert ""Nein"" und für das Attribut ""Ist Verkaufsartikel"" den Wert ""Ja"". Beispiel: Wenn Sie Laptops und Tragetaschen getrennt verkaufen und einen bestimmten Preis anbieten, wenn der Kunde beides zusammen kauft, dann wird der Laptop mit der Tasche zusammen ein neuer Bündel-Artikel. Anmerkung: BOM = Stückliste",
|
||||
Parent Item,Übergeordneter Artikel,
|
||||
List items that form the package.,"Die Artikel auflisten, die das Paket bilden.",
|
||||
SAL-QTN-.YYYY.-,SAL-QTN-.YYYY.-,
|
||||
Quotation To,Angebot für,
|
||||
Rate at which customer's currency is converted to company's base currency,"Kurs, zu dem die Währung des Kunden in die Basiswährung des Unternehmens umgerechnet wird",
|
||||
Rate at which Price list currency is converted to company's base currency,"Kurs, zu dem die Währung der Preisliste in die Basiswährung des Unternehmens umgerechnet wird",
|
||||
@@ -7704,7 +7702,6 @@ Quotation Item,Angebotsposition,
|
||||
Against Doctype,Zu DocType,
|
||||
Against Docname,Zu Dokumentenname,
|
||||
Additional Notes,Zusätzliche Bemerkungen,
|
||||
SAL-ORD-.YYYY.-,SAL-ORD-.YYYY.-,
|
||||
Skip Delivery Note,Lieferschein überspringen,
|
||||
In Words will be visible once you save the Sales Order.,"""In Worten"" wird sichtbar, sobald Sie den Auftrag speichern.",
|
||||
Track this Sales Order against any Project,Diesen Auftrag in jedem Projekt nachverfolgen,
|
||||
@@ -7935,7 +7932,7 @@ For reference,Zu Referenzzwecken,
|
||||
Territory Targets,Ziele für die Region,
|
||||
Set Item Group-wise budgets on this Territory. You can also include seasonality by setting the Distribution.,Artikelgruppenbezogene Budgets für diese Region erstellen. Durch Setzen der Auslieferungseinstellungen können auch saisonale Aspekte mit einbezogen werden.,
|
||||
UOM Name,Maßeinheit-Name,
|
||||
Check this to disallow fractions. (for Nos),"Hier aktivieren, um keine Bruchteile zuzulassen (für Nr.)",
|
||||
Check this to disallow fractions. (for Nos),"Hier aktivieren, um keine Bruchteile zuzulassen (für Anzahl)",
|
||||
Website Item Group,Webseiten-Artikelgruppe,
|
||||
Cross Listing of Item in multiple groups,Kreuzweise Auflistung des Artikels in mehreren Gruppen,
|
||||
Default settings for Shopping Cart,Standardeinstellungen für den Warenkorb,
|
||||
@@ -8016,7 +8013,6 @@ Contact Information,Kontaktinformationen,
|
||||
Email sent to,E-Mail versandt an,
|
||||
Dispatch Information,Versandinformationen,
|
||||
Estimated Arrival,Voraussichtliche Ankunft,
|
||||
MAT-DT-.YYYY.-,MAT-DT-.YYYY.-,
|
||||
Initial Email Notification Sent,Erste E-Mail-Benachrichtigung gesendet,
|
||||
Delivery Details,Lieferdetails,
|
||||
Driver Email,Fahrer-E-Mail,
|
||||
@@ -8176,7 +8172,6 @@ Purchase Receipt Item,Kaufbeleg-Artikel,
|
||||
Landed Cost Purchase Receipt,Einstandspreis-Kaufbeleg,
|
||||
Landed Cost Taxes and Charges,Einstandspreis Steuern und Gebühren,
|
||||
Landed Cost Voucher,Beleg über Einstandskosten,
|
||||
MAT-LCV-.YYYY.-,MAT-LCV-.YYYY.-,
|
||||
Purchase Receipts,Kaufbelege,
|
||||
Purchase Receipt Items,Kaufbeleg-Artikel,
|
||||
Get Items From Purchase Receipts,Artikel vom Kaufbeleg übernehmen,
|
||||
@@ -8184,7 +8179,6 @@ Distribute Charges Based On,Kosten auf folgender Grundlage verteilen,
|
||||
Landed Cost Help,Hilfe zum Einstandpreis,
|
||||
Manufacturers used in Items,Hersteller im Artikel verwendet,
|
||||
Limited to 12 characters,Limitiert auf 12 Zeichen,
|
||||
MAT-MR-.YYYY.-,MAT-MR-.YYYY.-,
|
||||
Partially Ordered,Teilweise bestellt,
|
||||
Transferred,Übergeben,
|
||||
% Ordered,% bestellt,
|
||||
@@ -8199,7 +8193,6 @@ Prevdoc DocType,Prevdoc DocType,
|
||||
Parent Detail docname,Übergeordnetes Detail Dokumentenname,
|
||||
"Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.","Packzettel für zu liefernde Pakete generieren. Wird verwendet, um Paketnummer, Packungsinhalt und das Gewicht zu dokumentieren.",
|
||||
Indicates that the package is a part of this delivery (Only Draft),"Zeigt an, dass das Paket ein Teil dieser Lieferung ist (nur Entwurf)",
|
||||
MAT-PAC-.YYYY.-,MAT-PAC-.YYYY.-,
|
||||
From Package No.,Von Paket Nr.,
|
||||
Identification of the package for the delivery (for print),Kennzeichnung des Paketes für die Lieferung (für den Druck),
|
||||
To Package No.,Bis Paket Nr.,
|
||||
@@ -8290,7 +8283,6 @@ Under AMC,Innerhalb des jährlichen Wartungsvertrags,
|
||||
Out of AMC,Außerhalb des jährlichen Wartungsvertrags,
|
||||
Warranty Period (Days),Garantiefrist (Tage),
|
||||
Serial No Details,Details zur Seriennummer,
|
||||
MAT-STE-.YYYY.-,MAT-STE-.JJJJ.-,
|
||||
Stock Entry Type,Bestandsbuchungsart,
|
||||
Stock Entry (Outward GIT),Bestandsbuchung (Outward GIT),
|
||||
Material Consumption for Manufacture,Materialverbrauch für die Herstellung,
|
||||
@@ -8336,7 +8328,6 @@ Stock Queue (FIFO),Lagerverfahren (FIFO),
|
||||
Is Cancelled,Ist storniert,
|
||||
Stock Reconciliation,Bestandsabgleich,
|
||||
This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.,"Dieses Werkzeug hilft Ihnen dabei, die Menge und die Bewertung von Bestand im System zu aktualisieren oder zu ändern. Es wird in der Regel verwendet, um die Systemwerte und den aktuellen Bestand Ihrer Lager zu synchronisieren.",
|
||||
MAT-RECO-.YYYY.-,MAT-RECO-.YYYY.-,
|
||||
Reconciliation JSON,Abgleich JSON (JavaScript Object Notation),
|
||||
Stock Reconciliation Item,Bestandsabgleich-Artikel,
|
||||
Before reconciliation,Vor Ausgleich,
|
||||
@@ -8796,8 +8787,7 @@ Availed ITC State/UT Tax,Verfügbare ITC State / UT Tax,
|
||||
Availed ITC Cess,ITC Cess verfügbar,
|
||||
Is Nil Rated or Exempted,Ist gleich Null oder ausgenommen,
|
||||
Is Non GST,Ist nicht GST,
|
||||
ACC-SINV-RET-.YYYY.-,ACC-SINV-RET-.YYYY.-,
|
||||
E-Way Bill No.,E-Way Bill No.,
|
||||
E-Way Bill No.,E-Way Bill Nr.,
|
||||
Is Consolidated,Ist konsolidiert,
|
||||
Billing Address GSTIN,Rechnungsadresse GSTIN,
|
||||
Customer GSTIN,Kunde GSTIN,
|
||||
@@ -9216,7 +9206,7 @@ Id,Ich würde,
|
||||
Time Required (In Mins),Erforderliche Zeit (in Minuten),
|
||||
From Posting Date,Ab dem Buchungsdatum,
|
||||
To Posting Date,Zum Buchungsdatum,
|
||||
No records found,Keine Aufzeichnungen gefunden,
|
||||
No records found,Keine Einträge gefunden,
|
||||
Customer/Lead Name,Name des Kunden / Lead,
|
||||
Unmarked Days,Nicht markierte Tage,
|
||||
Jan,Jan.,
|
||||
@@ -9275,7 +9265,7 @@ Delay (in Days),Verzögerung (in Tagen),
|
||||
Group by Sales Order,Nach Auftrag gruppieren,
|
||||
Sales Value,Verkaufswert,
|
||||
Stock Qty vs Serial No Count,Lagermenge vs Seriennummer,
|
||||
Serial No Count,Seriennummer nicht gezählt,
|
||||
Serial No Count,Seriennummern gezählt,
|
||||
Work Order Summary,Arbeitsauftragsübersicht,
|
||||
Produce Qty,Menge produzieren,
|
||||
Lead Time (in mins),Vorlaufzeit (in Minuten),
|
||||
@@ -9569,7 +9559,7 @@ Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atl
|
||||
You can alternatively disable selling price validation in {} to bypass this validation.,"Alternativ können Sie die Validierung des Verkaufspreises in {} deaktivieren, um diese Validierung zu umgehen.",
|
||||
Invalid Selling Price,Ungültiger Verkaufspreis,
|
||||
Address needs to be linked to a Company. Please add a row for Company in the Links table.,Die Adresse muss mit einem Unternehmen verknüpft sein. Bitte fügen Sie eine Zeile für Firma in die Tabelle Links ein.,
|
||||
Company Not Linked,Firma nicht verbunden,
|
||||
Company Not Linked,Firma nicht verknüpft,
|
||||
Import Chart of Accounts from CSV / Excel files,Kontenplan aus CSV / Excel-Dateien importieren,
|
||||
Completed Qty cannot be greater than 'Qty to Manufacture',Die abgeschlossene Menge darf nicht größer sein als die Menge bis zur Herstellung.,
|
||||
"Row {0}: For Supplier {1}, Email Address is Required to send an email","Zeile {0}: Für Lieferant {1} ist eine E-Mail-Adresse erforderlich, um eine E-Mail zu senden",
|
||||
@@ -9656,7 +9646,7 @@ Hide Customer's Tax ID from Sales Transactions,Steuer-ID des Kunden vor Verkaufs
|
||||
Action If Quality Inspection Is Not Submitted,Maßnahme Wenn keine Qualitätsprüfung eingereicht wird,
|
||||
Auto Insert Price List Rate If Missing,"Preisliste automatisch einfügen, falls fehlt",
|
||||
Automatically Set Serial Nos Based on FIFO,Seriennummern basierend auf FIFO automatisch einstellen,
|
||||
Set Qty in Transactions Based on Serial No Input,Stellen Sie die Menge in Transaktionen basierend auf Seriennummer ohne Eingabe ein,
|
||||
Set Qty in Transactions Based on Serial No Input,Setze die Anzahl in der Transaktion basierend auf den Seriennummern,
|
||||
Raise Material Request When Stock Reaches Re-order Level,"Erhöhen Sie die Materialanforderung, wenn der Lagerbestand die Nachbestellmenge erreicht",
|
||||
Notify by Email on Creation of Automatic Material Request,Benachrichtigen Sie per E-Mail über die Erstellung einer automatischen Materialanforderung,
|
||||
Allow Material Transfer from Delivery Note to Sales Invoice,Materialübertragung vom Lieferschein zur Ausgangsrechnung zulassen,
|
||||
@@ -9765,7 +9755,7 @@ Open Form View,Öffnen Sie die Formularansicht,
|
||||
POS invoice {0} created succesfully,POS-Rechnung {0} erfolgreich erstellt,
|
||||
Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.,Lagermenge nicht ausreichend für Artikelcode: {0} unter Lager {1}. Verfügbare Menge {2}.,
|
||||
Serial No: {0} has already been transacted into another POS Invoice.,Seriennummer: {0} wurde bereits in eine andere POS-Rechnung übertragen.,
|
||||
Balance Serial No,Balance Seriennr,
|
||||
Balance Serial No,Stand Seriennummern,
|
||||
Warehouse: {0} does not belong to {1},Lager: {0} gehört nicht zu {1},
|
||||
Please select batches for batched item {0},Bitte wählen Sie Chargen für Chargenartikel {0} aus,
|
||||
Please select quantity on row {0},Bitte wählen Sie die Menge in Zeile {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user