mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-21 14:09:19 +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:
|
if frappe.flags.in_test:
|
||||||
make_dimension_in_accounting_doctypes(doc=self)
|
make_dimension_in_accounting_doctypes(doc=self)
|
||||||
else:
|
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):
|
def on_trash(self):
|
||||||
if frappe.flags.in_test:
|
if frappe.flags.in_test:
|
||||||
delete_accounting_dimension(doc=self)
|
delete_accounting_dimension(doc=self)
|
||||||
else:
|
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):
|
def set_fieldname_and_label(self):
|
||||||
if not self.label:
|
if not self.label:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"book_tax_discount_loss",
|
"book_tax_discount_loss",
|
||||||
"print_settings",
|
"print_settings",
|
||||||
"show_inclusive_tax_in_print",
|
"show_inclusive_tax_in_print",
|
||||||
|
"show_taxes_as_table_in_print",
|
||||||
"column_break_12",
|
"column_break_12",
|
||||||
"show_payment_schedule_in_print",
|
"show_payment_schedule_in_print",
|
||||||
"currency_exchange_section",
|
"currency_exchange_section",
|
||||||
@@ -378,6 +379,12 @@
|
|||||||
"fieldname": "auto_reconcile_payments",
|
"fieldname": "auto_reconcile_payments",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Auto Reconcile Payments"
|
"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",
|
"icon": "icon-cog",
|
||||||
@@ -385,7 +392,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-04-21 13:11:37.130743",
|
"modified": "2023-06-13 18:47:46.430291",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
@@ -414,4 +421,4 @@
|
|||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"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) {
|
get_entries: function(frm, account) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "get_accounts_data",
|
method: "get_accounts_data",
|
||||||
@@ -126,7 +141,8 @@ var get_account_details = function(frm, cdt, cdn) {
|
|||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
posting_date: frm.doc.posting_date,
|
posting_date: frm.doc.posting_date,
|
||||||
party_type: row.party_type,
|
party_type: row.party_type,
|
||||||
party: row.party
|
party: row.party,
|
||||||
|
rounding_loss_allowance: frm.doc.rounding_loss_allowance
|
||||||
},
|
},
|
||||||
callback: function(r){
|
callback: function(r){
|
||||||
$.extend(row, r.message);
|
$.extend(row, r.message);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"posting_date",
|
"posting_date",
|
||||||
|
"rounding_loss_allowance",
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
"company",
|
"company",
|
||||||
"section_break_4",
|
"section_break_4",
|
||||||
@@ -96,11 +97,18 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_10",
|
"fieldname": "column_break_10",
|
||||||
"fieldtype": "Column Break"
|
"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,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-12-29 19:38:24.416529",
|
"modified": "2023-06-12 21:02:09.818208",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Exchange Rate Revaluation",
|
"name": "Exchange Rate Revaluation",
|
||||||
|
|||||||
@@ -18,8 +18,13 @@ from erpnext.setup.utils import get_exchange_rate
|
|||||||
|
|
||||||
class ExchangeRateRevaluation(Document):
|
class ExchangeRateRevaluation(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_rounding_loss_allowance()
|
||||||
self.set_total_gain_loss()
|
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):
|
def set_total_gain_loss(self):
|
||||||
total_gain_loss = 0
|
total_gain_loss = 0
|
||||||
|
|
||||||
@@ -92,7 +97,12 @@ class ExchangeRateRevaluation(Document):
|
|||||||
def get_accounts_data(self):
|
def get_accounts_data(self):
|
||||||
self.validate_mandatory()
|
self.validate_mandatory()
|
||||||
account_details = self.get_account_balance_from_gle(
|
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(
|
accounts_with_new_balance = self.calculate_new_account_balance(
|
||||||
self.company, self.posting_date, account_details
|
self.company, self.posting_date, account_details
|
||||||
@@ -104,7 +114,9 @@ class ExchangeRateRevaluation(Document):
|
|||||||
return accounts_with_new_balance
|
return accounts_with_new_balance
|
||||||
|
|
||||||
@staticmethod
|
@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 = []
|
account_details = []
|
||||||
|
|
||||||
if company and posting_date:
|
if company and posting_date:
|
||||||
@@ -172,10 +184,18 @@ class ExchangeRateRevaluation(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# round off balance based on currency precision
|
# round off balance based on currency precision
|
||||||
|
# and consider debit-credit difference allowance
|
||||||
currency_precision = get_currency_precision()
|
currency_precision = get_currency_precision()
|
||||||
|
rounding_loss_allowance = rounding_loss_allowance or 0.05
|
||||||
for acc in account_details:
|
for acc in account_details:
|
||||||
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
|
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)
|
acc.balance = flt(acc.balance, currency_precision)
|
||||||
|
if abs(acc.balance) <= rounding_loss_allowance:
|
||||||
|
acc.balance = 0
|
||||||
|
|
||||||
acc.zero_balance = (
|
acc.zero_balance = (
|
||||||
True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False
|
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()
|
@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):
|
if not (company and posting_date):
|
||||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
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_currency": account_currency,
|
||||||
}
|
}
|
||||||
account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
|
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 (
|
if account_balance and (
|
||||||
|
|||||||
@@ -940,6 +940,7 @@ class JournalEntry(AccountsController):
|
|||||||
blank_row.debit_in_account_currency = abs(diff)
|
blank_row.debit_in_account_currency = abs(diff)
|
||||||
blank_row.debit = abs(diff)
|
blank_row.debit = abs(diff)
|
||||||
|
|
||||||
|
self.set_total_debit_credit()
|
||||||
self.validate_total_debit_and_credit()
|
self.validate_total_debit_and_credit()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -148,19 +148,57 @@ class PaymentEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_allocated_amount(self):
|
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)) > 0:
|
||||||
if flt(d.allocated_amount) > flt(d.outstanding_amount):
|
if flt(d.allocated_amount) > flt(d.outstanding_amount):
|
||||||
frappe.throw(
|
frappe.throw(fail_message.format(d.idx))
|
||||||
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for negative outstanding invoices as well
|
# Check for negative outstanding invoices as well
|
||||||
if flt(d.allocated_amount) < 0:
|
if flt(d.allocated_amount) < 0:
|
||||||
if flt(d.allocated_amount) < flt(d.outstanding_amount):
|
if flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||||
frappe.throw(
|
frappe.throw(fail_message.format(d.idx))
|
||||||
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
|
|
||||||
)
|
|
||||||
|
|
||||||
def delink_advance_entry_references(self):
|
def delink_advance_entry_references(self):
|
||||||
for reference in self.references:
|
for reference in self.references:
|
||||||
@@ -373,7 +411,7 @@ class PaymentEntry(AccountsController):
|
|||||||
for k, v in no_oustanding_refs.items():
|
for k, v in no_oustanding_refs.items():
|
||||||
frappe.msgprint(
|
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(
|
).format(
|
||||||
_(k),
|
_(k),
|
||||||
frappe.bold(", ".join(d.reference_name for d in v)),
|
frappe.bold(", ".join(d.reference_name for d in v)),
|
||||||
@@ -1449,7 +1487,7 @@ def get_orders_to_be_billed(
|
|||||||
if voucher_type:
|
if voucher_type:
|
||||||
doc = frappe.get_doc({"doctype": voucher_type})
|
doc = frappe.get_doc({"doctype": voucher_type})
|
||||||
condition = ""
|
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
|
condition = " and cost_center='%s'" % cost_center
|
||||||
|
|
||||||
orders = []
|
orders = []
|
||||||
@@ -1495,9 +1533,15 @@ def get_orders_to_be_billed(
|
|||||||
|
|
||||||
order_list = []
|
order_list = []
|
||||||
for d in orders:
|
for d in orders:
|
||||||
if not (
|
if (
|
||||||
flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
|
filters
|
||||||
and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))
|
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
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -1013,6 +1013,30 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
employee = make_employee("test_payment_entry@salary.com", company="_Test Company")
|
employee = make_employee("test_payment_entry@salary.com", company="_Test Company")
|
||||||
create_payment_entry(party_type="Employee", party=employee, save=True)
|
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):
|
def create_payment_entry(**args):
|
||||||
payment_entry = frappe.new_doc("Payment Entry")
|
payment_entry = frappe.new_doc("Payment Entry")
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import frappe
|
|||||||
from frappe import _, msgprint, qb
|
from frappe import _, msgprint, qb
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
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
|
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -127,12 +126,29 @@ class PaymentReconciliation(Document):
|
|||||||
|
|
||||||
return list(journal_entries)
|
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):
|
def get_dr_or_cr_notes(self):
|
||||||
|
|
||||||
self.build_qb_filter_conditions(get_return_invoices=True)
|
self.build_qb_filter_conditions(get_return_invoices=True)
|
||||||
|
|
||||||
ple = qb.DocType("Payment Ledger Entry")
|
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":
|
if erpnext.get_party_account_type(self.party_type) == "Receivable":
|
||||||
self.common_filter_conditions.append(ple.account_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_type == "Payable")
|
||||||
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
||||||
|
|
||||||
# get return invoices
|
self.get_return_invoices()
|
||||||
doc = qb.DocType(voucher_type)
|
return_invoices = [
|
||||||
return_invoices = (
|
x for x in self.return_invoices if x.return_against == None or x.return_against == ""
|
||||||
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)
|
|
||||||
)
|
|
||||||
|
|
||||||
outstanding_dr_or_cr = []
|
outstanding_dr_or_cr = []
|
||||||
if return_invoices:
|
if return_invoices:
|
||||||
@@ -204,6 +211,15 @@ class PaymentReconciliation(Document):
|
|||||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
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:
|
if self.invoice_limit:
|
||||||
non_reconciled_invoices = non_reconciled_invoices[: 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_type="Period Closing Voucher",
|
||||||
voucher_no=self.name,
|
voucher_no=self.name,
|
||||||
queue="long",
|
queue="long",
|
||||||
|
enqueue_after_commit=True,
|
||||||
)
|
)
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
|
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, getdate
|
from frappe.utils import cint, flt, getdate
|
||||||
|
|
||||||
|
|
||||||
class TaxWithholdingCategory(Document):
|
class TaxWithholdingCategory(Document):
|
||||||
@@ -569,7 +569,12 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
|
|||||||
tds_amount = 0
|
tds_amount = 0
|
||||||
limit_consumed = frappe.db.get_value(
|
limit_consumed = frappe.db.get_value(
|
||||||
"Purchase Invoice",
|
"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)",
|
"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):
|
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
|
return current_amount * rate / 100
|
||||||
else:
|
else:
|
||||||
ltds_amount = certificate_limit - deducted_amount
|
ltds_amount = certificate_limit - flt(deducted_amount)
|
||||||
tds_amount = current_amount - ltds_amount
|
tds_amount = current_amount - ltds_amount
|
||||||
|
|
||||||
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
|
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
|
||||||
@@ -598,9 +603,9 @@ def is_valid_certificate(
|
|||||||
):
|
):
|
||||||
valid = False
|
valid = False
|
||||||
|
|
||||||
if (
|
available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_amount)
|
||||||
getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)
|
|
||||||
) and certificate_limit > deducted_amount:
|
if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
|
||||||
valid = True
|
valid = True
|
||||||
|
|
||||||
return valid
|
return valid
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint, scrub
|
from frappe import _, msgprint, scrub
|
||||||
from frappe.contacts.doctype.address.address import (
|
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
|
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.
|
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`.
|
and/or `is_shipping_address = 1`.
|
||||||
@@ -861,22 +863,23 @@ def get_party_shipping_address(doctype, name):
|
|||||||
:param name: Party name
|
:param name: Party name
|
||||||
:return: String
|
:return: String
|
||||||
"""
|
"""
|
||||||
out = frappe.db.sql(
|
shipping_addresses = frappe.get_all(
|
||||||
"SELECT dl.parent "
|
"Address",
|
||||||
"from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name "
|
filters=[
|
||||||
"where "
|
["Dynamic Link", "link_doctype", "=", doctype],
|
||||||
"dl.link_doctype=%s "
|
["Dynamic Link", "link_name", "=", name],
|
||||||
"and dl.link_name=%s "
|
["disabled", "=", 0],
|
||||||
"and dl.parenttype='Address' "
|
],
|
||||||
"and ifnull(ta.disabled, 0) = 0 and"
|
or_filters=[
|
||||||
"(ta.address_type='Shipping' or ta.is_shipping_address=1) "
|
["is_shipping_address", "=", 1],
|
||||||
"order by ta.is_shipping_address desc, ta.address_type desc limit 1",
|
["address_type", "=", "Shipping"],
|
||||||
(doctype, name),
|
],
|
||||||
|
pluck="name",
|
||||||
|
limit=1,
|
||||||
|
order_by="is_shipping_address DESC",
|
||||||
)
|
)
|
||||||
if out:
|
|
||||||
return out[0][0]
|
return shipping_addresses[0] if shipping_addresses else None
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def get_partywise_advanced_payment_amount(
|
def get_partywise_advanced_payment_amount(
|
||||||
@@ -910,31 +913,32 @@ def get_partywise_advanced_payment_amount(
|
|||||||
return frappe._dict(data)
|
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.
|
Returns contact name only if there is a primary contact for given doctype and name.
|
||||||
Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact.
|
|
||||||
|
Else returns None
|
||||||
|
|
||||||
|
:param doctype: Party Doctype
|
||||||
|
:param name: Party name
|
||||||
|
:return: String
|
||||||
"""
|
"""
|
||||||
out = frappe.db.sql(
|
contacts = frappe.get_all(
|
||||||
"""
|
"Contact",
|
||||||
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact
|
filters=[
|
||||||
FROM `tabDynamic Link` dl
|
["Dynamic Link", "link_doctype", "=", doctype],
|
||||||
INNER JOIN `tabContact` c ON c.name = dl.parent
|
["Dynamic Link", "link_name", "=", name],
|
||||||
WHERE
|
],
|
||||||
dl.link_doctype=%s AND
|
or_filters=[
|
||||||
dl.link_name=%s AND
|
["is_primary_contact", "=", 1],
|
||||||
dl.parenttype = 'Contact'
|
["is_billing_contact", "=", 1],
|
||||||
ORDER BY is_primary_contact DESC, is_billing_contact DESC
|
],
|
||||||
""",
|
pluck="name",
|
||||||
(doctype, name),
|
limit=1,
|
||||||
|
order_by="is_primary_contact DESC, is_billing_contact DESC",
|
||||||
)
|
)
|
||||||
if out:
|
|
||||||
try:
|
return contacts[0] if contacts else None
|
||||||
return out[0][0]
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def add_party_account(party_type, party, company, account):
|
def add_party_account(party_type, party, company, account):
|
||||||
|
|||||||
@@ -181,6 +181,16 @@ class ReceivablePayableReport(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
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)
|
row = self.voucher_balance.get(key)
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
@@ -610,7 +620,7 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
def get_return_entries(self):
|
def get_return_entries(self):
|
||||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
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)
|
party_field = scrub(self.filters.party_type)
|
||||||
if self.filters.get(party_field):
|
if self.filters.get(party_field):
|
||||||
filters.update({party_field: 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):
|
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
@@ -256,7 +317,7 @@ def make_payment(docname):
|
|||||||
|
|
||||||
|
|
||||||
def make_credit_note(docname):
|
def make_credit_note(docname):
|
||||||
create_sales_invoice(
|
credit_note = create_sales_invoice(
|
||||||
company="_Test Company 2",
|
company="_Test Company 2",
|
||||||
customer="_Test Customer 2",
|
customer="_Test Customer 2",
|
||||||
currency="EUR",
|
currency="EUR",
|
||||||
@@ -269,3 +330,5 @@ def make_credit_note(docname):
|
|||||||
is_return=1,
|
is_return=1,
|
||||||
return_against=docname,
|
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`.posting_date, `tabSales Invoice`.debit_to,
|
||||||
`tabSales Invoice`.unrealized_profit_loss_account,
|
`tabSales Invoice`.unrealized_profit_loss_account,
|
||||||
`tabSales Invoice`.is_internal_customer,
|
`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`.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_code, `tabSales Invoice Item`.description,
|
||||||
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
||||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
||||||
|
|||||||
@@ -917,6 +917,9 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
return is_inclusive
|
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):
|
def validate_advance_entries(self):
|
||||||
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
|
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)))
|
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(
|
doc.print_templates.update(
|
||||||
{
|
{
|
||||||
"total": "templates/print_formats/includes/total.html",
|
"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):
|
def format_columns(display_columns, compact_fields):
|
||||||
compact_fields = compact_fields + ["image", "item_code", "item_name"]
|
compact_fields = compact_fields + ["image", "item_code", "item_name"]
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
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.email.inbox import link_communication_to_document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
|
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()
|
self.update_prospect()
|
||||||
|
|
||||||
def on_trash(self):
|
def on_trash(self):
|
||||||
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
|
frappe.db.set_value("Issue", {"lead": self.name}, "lead", None)
|
||||||
|
delete_contact_and_address(self.doctype, self.name)
|
||||||
self.unlink_dynamic_links()
|
|
||||||
self.remove_link_from_prospect()
|
self.remove_link_from_prospect()
|
||||||
|
|
||||||
def set_full_name(self):
|
def set_full_name(self):
|
||||||
@@ -122,27 +124,6 @@ class Lead(SellingController, CRMNote):
|
|||||||
)
|
)
|
||||||
lead_row.db_update()
|
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):
|
def remove_link_from_prospect(self):
|
||||||
prospects = self.get_linked_prospects()
|
prospects = self.get_linked_prospects()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
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 frappe.model.mapper import get_mapped_doc
|
||||||
|
|
||||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
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()
|
self.link_with_lead_contact_and_address()
|
||||||
|
|
||||||
def on_trash(self):
|
def on_trash(self):
|
||||||
self.unlink_dynamic_links()
|
delete_contact_and_address(self.doctype, self.name)
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
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.append("links", {"link_doctype": self.doctype, "link_name": self.name})
|
||||||
linked_doc.save(ignore_permissions=True)
|
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()
|
@frappe.whitelist()
|
||||||
def make_customer(source_name, target_doc=None):
|
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;">`;
|
let title_html = `<div style="display: flex; margin-left: -15px;">`;
|
||||||
title_html += `
|
title_html += `
|
||||||
<div class="col-8" style="margin-right: -15px;">
|
<div class="col-8" style="margin-right: -15px;">
|
||||||
<a class="" href="/${ item.route || '#' }"
|
<a href="/${ item.route || '#' }">
|
||||||
style="color: var(--gray-800); font-weight: 500;">
|
<div class="product-title">
|
||||||
${ title }
|
${ title }
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -201,4 +202,4 @@ erpnext.ProductList = class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,12 +88,14 @@ class BOMUpdateLog(Document):
|
|||||||
boms=boms,
|
boms=boms,
|
||||||
timeout=40000,
|
timeout=40000,
|
||||||
now=frappe.flags.in_test,
|
now=frappe.flags.in_test,
|
||||||
|
enqueue_after_commit=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
frappe.enqueue(
|
frappe.enqueue(
|
||||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
||||||
update_doc=self,
|
update_doc=self,
|
||||||
now=frappe.flags.in_test,
|
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
|
// and if stock mvt for WIP is required
|
||||||
if (frm.doc.work_order) {
|
if (frm.doc.work_order) {
|
||||||
frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => {
|
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");
|
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");
|
refresh_field("total_completed_qty");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"time_logs",
|
"time_logs",
|
||||||
"section_break_13",
|
"section_break_13",
|
||||||
"total_completed_qty",
|
"total_completed_qty",
|
||||||
|
"process_loss_qty",
|
||||||
"column_break_15",
|
"column_break_15",
|
||||||
"total_time_in_mins",
|
"total_time_in_mins",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
@@ -435,11 +436,17 @@
|
|||||||
"fieldname": "expected_end_date",
|
"fieldname": "expected_end_date",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "Expected End Date"
|
"label": "Expected End Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "process_loss_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Process Loss Qty",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-05-23 09:56:43.826602",
|
"modified": "2023-06-09 12:04:55.534264",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card",
|
"name": "Job Card",
|
||||||
@@ -497,4 +504,4 @@
|
|||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "operation",
|
"title_field": "operation",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class JobCard(Document):
|
|||||||
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
|
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
|
||||||
|
|
||||||
for row in self.sub_operations:
|
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):
|
def get_overlap_for(self, args, check_next_available_slot=False):
|
||||||
production_capacity = 1
|
production_capacity = 1
|
||||||
@@ -451,6 +451,9 @@ class JobCard(Document):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def before_save(self):
|
||||||
|
self.set_process_loss()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_transfer_qty()
|
self.validate_transfer_qty()
|
||||||
self.validate_job_card()
|
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"))
|
total_completed_qty = bold(_("Total Completed Qty"))
|
||||||
qty_to_manufacture = bold(_("Qty to Manufacture"))
|
qty_to_manufacture = bold(_("Qty to Manufacture"))
|
||||||
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("The {0} ({1}) must be equal to {2} ({3})").format(
|
_("The {0} ({1}) must be equal to {2} ({3})").format(
|
||||||
total_completed_qty,
|
total_completed_qty,
|
||||||
bold(self.total_completed_qty),
|
bold(flt(total_completed_qty, precision)),
|
||||||
qty_to_manufacture,
|
qty_to_manufacture,
|
||||||
bold(self.for_quantity),
|
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):
|
def update_work_order(self):
|
||||||
if not self.work_order:
|
if not self.work_order:
|
||||||
return
|
return
|
||||||
@@ -511,7 +530,7 @@ class JobCard(Document):
|
|||||||
):
|
):
|
||||||
return
|
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 = [], []
|
from_time_list, to_time_list = [], []
|
||||||
|
|
||||||
field = "operation_id"
|
field = "operation_id"
|
||||||
@@ -519,6 +538,7 @@ class JobCard(Document):
|
|||||||
if data and len(data) > 0:
|
if data and len(data) > 0:
|
||||||
for_quantity = flt(data[0].completed_qty)
|
for_quantity = flt(data[0].completed_qty)
|
||||||
time_in_mins = flt(data[0].time_in_mins)
|
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)
|
wo = frappe.get_doc("Work Order", self.work_order)
|
||||||
|
|
||||||
@@ -526,8 +546,8 @@ class JobCard(Document):
|
|||||||
self.update_corrective_in_work_order(wo)
|
self.update_corrective_in_work_order(wo)
|
||||||
|
|
||||||
elif self.operation_id:
|
elif self.operation_id:
|
||||||
self.validate_produced_quantity(for_quantity, wo)
|
self.validate_produced_quantity(for_quantity, process_loss_qty, wo)
|
||||||
self.update_work_order_data(for_quantity, time_in_mins, wo)
|
self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo)
|
||||||
|
|
||||||
def update_corrective_in_work_order(self, wo):
|
def update_corrective_in_work_order(self, wo):
|
||||||
wo.corrective_operation_cost = 0.0
|
wo.corrective_operation_cost = 0.0
|
||||||
@@ -542,11 +562,11 @@ class JobCard(Document):
|
|||||||
wo.flags.ignore_validate_update_after_submit = True
|
wo.flags.ignore_validate_update_after_submit = True
|
||||||
wo.save()
|
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:
|
if self.docstatus < 2:
|
||||||
return
|
return
|
||||||
|
|
||||||
if wo.produced_qty > for_quantity:
|
if wo.produced_qty > for_quantity + process_loss_qty:
|
||||||
first_part_msg = _(
|
first_part_msg = _(
|
||||||
"The {0} {1} is used to calculate the valuation cost for the finished good {2}."
|
"The {0} {1} is used to calculate the valuation cost for the finished good {2}."
|
||||||
).format(
|
).format(
|
||||||
@@ -561,7 +581,7 @@ class JobCard(Document):
|
|||||||
_("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error")
|
_("{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")
|
workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate")
|
||||||
jc = frappe.qb.DocType("Job Card")
|
jc = frappe.qb.DocType("Job Card")
|
||||||
jctl = frappe.qb.DocType("Job Card Time Log")
|
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||||
@@ -582,6 +602,7 @@ class JobCard(Document):
|
|||||||
for data in wo.operations:
|
for data in wo.operations:
|
||||||
if data.get("name") == self.operation_id:
|
if data.get("name") == self.operation_id:
|
||||||
data.completed_qty = for_quantity
|
data.completed_qty = for_quantity
|
||||||
|
data.process_loss_qty = process_loss_qty
|
||||||
data.actual_operation_time = time_in_mins
|
data.actual_operation_time = time_in_mins
|
||||||
data.actual_start_time = time_data[0].start_time if time_data else None
|
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
|
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):
|
def get_current_operation_data(self):
|
||||||
return frappe.get_all(
|
return frappe.get_all(
|
||||||
"Job Card",
|
"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={
|
filters={
|
||||||
"docstatus": 1,
|
"docstatus": 1,
|
||||||
"work_order": self.work_order,
|
"work_order": self.work_order,
|
||||||
@@ -777,7 +802,7 @@ class JobCard(Document):
|
|||||||
|
|
||||||
data = frappe.get_all(
|
data = frappe.get_all(
|
||||||
"Work Order Operation",
|
"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)},
|
filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)},
|
||||||
order_by="sequence_id, idx",
|
order_by="sequence_id, idx",
|
||||||
)
|
)
|
||||||
@@ -795,6 +820,16 @@ class JobCard(Document):
|
|||||||
OperationSequenceError,
|
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):
|
def validate_work_order(self):
|
||||||
if self.is_work_order_closed():
|
if self.is_work_order_closed():
|
||||||
frappe.throw(_("You can't make any changes to Job Card since Work Order is 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
|
from typing import Literal
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.test_runner import make_test_records
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import random_string
|
from frappe.utils import random_string
|
||||||
from frappe.utils.data import add_to_date, now, today
|
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.from_bom, 1.0)
|
||||||
self.assertEqual(ste.bom_no, work_order.bom_no)
|
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():
|
def create_bom_with_multiple_operations():
|
||||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ def setup_bom(**args):
|
|||||||
routing=args.routing,
|
routing=args.routing,
|
||||||
with_operations=1,
|
with_operations=1,
|
||||||
currency=args.currency,
|
currency=args.currency,
|
||||||
|
source_warehouse=args.source_warehouse,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
bom_doc = frappe.get_doc("BOM", name)
|
bom_doc = frappe.get_doc("BOM", name)
|
||||||
|
|||||||
@@ -891,7 +891,7 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
self.assertEqual(se.process_loss_qty, 1)
|
self.assertEqual(se.process_loss_qty, 1)
|
||||||
|
|
||||||
wo.load_from_db()
|
wo.load_from_db()
|
||||||
self.assertEqual(wo.status, "In Process")
|
self.assertEqual(wo.status, "Completed")
|
||||||
|
|
||||||
@timeout(seconds=60)
|
@timeout(seconds=60)
|
||||||
def test_job_card_scrap_item(self):
|
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.status != "Closed") {
|
||||||
if (frm.doc.docstatus === 1
|
if (frm.doc.docstatus === 1 && frm.doc.status !== "Completed"
|
||||||
&& frm.doc.operations && frm.doc.operations.length) {
|
&& frm.doc.operations && frm.doc.operations.length) {
|
||||||
|
|
||||||
const not_completed = frm.doc.operations.filter(d => {
|
const not_completed = frm.doc.operations.filter(d => {
|
||||||
@@ -256,6 +256,12 @@ frappe.ui.form.on("Work Order", {
|
|||||||
label: __('Batch Size'),
|
label: __('Batch Size'),
|
||||||
read_only: 1
|
read_only: 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: 'Int',
|
||||||
|
fieldname: 'sequence_id',
|
||||||
|
label: __('Sequence Id'),
|
||||||
|
read_only: 1
|
||||||
|
},
|
||||||
],
|
],
|
||||||
data: operations_data,
|
data: operations_data,
|
||||||
in_place_edit: true,
|
in_place_edit: true,
|
||||||
@@ -280,8 +286,8 @@ frappe.ui.form.on("Work Order", {
|
|||||||
|
|
||||||
var pending_qty = 0;
|
var pending_qty = 0;
|
||||||
frm.doc.operations.forEach(data => {
|
frm.doc.operations.forEach(data => {
|
||||||
if(data.completed_qty != frm.doc.qty) {
|
if(data.completed_qty + data.process_loss_qty != frm.doc.qty) {
|
||||||
pending_qty = frm.doc.qty - flt(data.completed_qty);
|
pending_qty = frm.doc.qty - flt(data.completed_qty) - flt(data.process_loss_qty);
|
||||||
|
|
||||||
if (pending_qty) {
|
if (pending_qty) {
|
||||||
dialog.fields_dict.operations.df.data.push({
|
dialog.fields_dict.operations.df.data.push({
|
||||||
@@ -290,7 +296,8 @@ frappe.ui.form.on("Work Order", {
|
|||||||
'workstation': data.workstation,
|
'workstation': data.workstation,
|
||||||
'batch_size': data.batch_size,
|
'batch_size': data.batch_size,
|
||||||
'qty': pending_qty,
|
'qty': pending_qty,
|
||||||
'pending_qty': pending_qty
|
'pending_qty': pending_qty,
|
||||||
|
'sequence_id': data.sequence_id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@
|
|||||||
"required_items_section",
|
"required_items_section",
|
||||||
"materials_and_operations_tab",
|
"materials_and_operations_tab",
|
||||||
"operations_section",
|
"operations_section",
|
||||||
"operations",
|
|
||||||
"transfer_material_against",
|
"transfer_material_against",
|
||||||
|
"operations",
|
||||||
"time",
|
"time",
|
||||||
"planned_start_date",
|
"planned_start_date",
|
||||||
"planned_end_date",
|
"planned_end_date",
|
||||||
@@ -331,7 +331,6 @@
|
|||||||
"label": "Expected Delivery Date"
|
"label": "Expected Delivery Date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "operations_section",
|
"fieldname": "operations_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Operations",
|
"label": "Operations",
|
||||||
@@ -599,7 +598,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-04-06 12:35:12.149827",
|
"modified": "2023-06-09 13:20:09.154362",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order",
|
"name": "Work Order",
|
||||||
|
|||||||
@@ -249,7 +249,9 @@ class WorkOrder(Document):
|
|||||||
status = "Not Started"
|
status = "Not Started"
|
||||||
if flt(self.material_transferred_for_manufacturing) > 0:
|
if flt(self.material_transferred_for_manufacturing) > 0:
|
||||||
status = "In Process"
|
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"
|
status = "Completed"
|
||||||
else:
|
else:
|
||||||
status = "Cancelled"
|
status = "Cancelled"
|
||||||
@@ -736,13 +738,15 @@ class WorkOrder(Document):
|
|||||||
max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty))
|
max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty))
|
||||||
|
|
||||||
for d in self.get("operations"):
|
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"
|
d.status = "Pending"
|
||||||
elif flt(d.completed_qty) < flt(self.qty):
|
elif flt(qty) < flt(self.qty):
|
||||||
d.status = "Work in Progress"
|
d.status = "Work in Progress"
|
||||||
elif flt(d.completed_qty) == flt(self.qty):
|
elif flt(qty) == flt(self.qty):
|
||||||
d.status = "Completed"
|
d.status = "Completed"
|
||||||
elif flt(d.completed_qty) <= max_allowed_qty_for_wo:
|
elif flt(qty) <= max_allowed_qty_for_wo:
|
||||||
d.status = "Completed"
|
d.status = "Completed"
|
||||||
else:
|
else:
|
||||||
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))
|
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
"actions": [],
|
"actions": [],
|
||||||
"creation": "2014-10-16 14:35:41.950175",
|
"creation": "2014-10-16 14:35:41.950175",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"details",
|
"details",
|
||||||
"operation",
|
"operation",
|
||||||
"status",
|
"status",
|
||||||
"completed_qty",
|
"completed_qty",
|
||||||
|
"process_loss_qty",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"bom",
|
"bom",
|
||||||
"workstation_type",
|
"workstation_type",
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"columns": 2,
|
||||||
"fieldname": "operation",
|
"fieldname": "operation",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -46,6 +49,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"columns": 2,
|
||||||
"fieldname": "bom",
|
"fieldname": "bom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -62,7 +66,7 @@
|
|||||||
"oldfieldtype": "Text"
|
"oldfieldtype": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 1,
|
"columns": 2,
|
||||||
"description": "Operation completed for how many finished goods?",
|
"description": "Operation completed for how many finished goods?",
|
||||||
"fieldname": "completed_qty",
|
"fieldname": "completed_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
@@ -80,6 +84,7 @@
|
|||||||
"options": "Pending\nWork in Progress\nCompleted"
|
"options": "Pending\nWork in Progress\nCompleted"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"columns": 1,
|
||||||
"fieldname": "workstation",
|
"fieldname": "workstation",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -115,7 +120,7 @@
|
|||||||
"fieldname": "time_in_mins",
|
"fieldname": "time_in_mins",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Operation Time",
|
"label": "Time",
|
||||||
"oldfieldname": "time_in_mins",
|
"oldfieldname": "time_in_mins",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
@@ -203,12 +208,21 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Workstation Type",
|
"label": "Workstation Type",
|
||||||
"options": "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,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-09 01:37:56.563068",
|
"modified": "2023-06-09 14:03:01.612909",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Work Order Operation",
|
"name": "Work Order Operation",
|
||||||
|
|||||||
@@ -805,11 +805,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frm.doc.payments.find(pay => {
|
if(!this.frm.doc.is_return){
|
||||||
if (pay.default) {
|
this.frm.doc.payments.find(payment => {
|
||||||
pay.amount = total_amount_to_pay;
|
if (payment.default) {
|
||||||
}
|
payment.amount = total_amount_to_pay;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.frm.refresh_fields();
|
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.flags.ignore_permissions = ignore_permissions
|
||||||
|
target.delivery_date = nowdate()
|
||||||
target.run_method("set_missing_values")
|
target.run_method("set_missing_values")
|
||||||
target.run_method("calculate_taxes_and_totals")
|
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)
|
balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0)
|
||||||
target.qty = balance_qty if balance_qty > 0 else 0
|
target.qty = balance_qty if balance_qty > 0 else 0
|
||||||
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
|
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
|
||||||
|
target.delivery_date = nowdate()
|
||||||
|
|
||||||
if obj.against_blanket_order:
|
if obj.against_blanket_order:
|
||||||
target.against_blanket_order = 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 = make_sales_order(quotation.name)
|
||||||
sales_order.currency = "USD"
|
sales_order.currency = "USD"
|
||||||
sales_order.conversion_rate = 20.0
|
sales_order.conversion_rate = 20.0
|
||||||
sales_order.delivery_date = "2019-01-01"
|
|
||||||
sales_order.naming_series = "_T-Quotation-"
|
sales_order.naming_series = "_T-Quotation-"
|
||||||
sales_order.transaction_date = nowdate()
|
sales_order.transaction_date = nowdate()
|
||||||
|
sales_order.delivery_date = nowdate()
|
||||||
sales_order.insert()
|
sales_order.insert()
|
||||||
|
|
||||||
self.assertEqual(sales_order.currency, "USD")
|
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:
|
if not args.do_not_save:
|
||||||
qo.insert()
|
qo.insert()
|
||||||
if not args.do_not_submit:
|
if not args.do_not_submit:
|
||||||
|
|||||||
@@ -158,7 +158,8 @@ class SalesOrder(SellingController):
|
|||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("Expected Delivery Date should be after Sales Order Date"),
|
_("Expected Delivery Date should be after Sales Order Date"),
|
||||||
indicator="orange",
|
indicator="orange",
|
||||||
title=_("Warning"),
|
title=_("Invalid Delivery Date"),
|
||||||
|
raise_exception=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
frappe.throw(_("Please enter Delivery Date"))
|
frappe.throw(_("Please enter Delivery Date"))
|
||||||
@@ -217,6 +218,7 @@ class SalesOrder(SellingController):
|
|||||||
frappe.throw(_("Quotation {0} is cancelled").format(quotation))
|
frappe.throw(_("Quotation {0} is cancelled").format(quotation))
|
||||||
|
|
||||||
doc.set_status(update=True)
|
doc.set_status(update=True)
|
||||||
|
doc.update_opportunity("Converted" if flag == "submit" else "Quotation")
|
||||||
|
|
||||||
def validate_drop_ship(self):
|
def validate_drop_ship(self):
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
|
|||||||
@@ -215,13 +215,39 @@ def hide_workspaces():
|
|||||||
|
|
||||||
|
|
||||||
def create_default_role_profiles():
|
def create_default_role_profiles():
|
||||||
for module in ["Accounts", "Stock", "Manufacturing"]:
|
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
|
||||||
create_role_profile(module)
|
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):
|
DEFAULT_ROLE_PROFILES = {
|
||||||
role_profile = frappe.new_doc("Role Profile")
|
"Inventory": [
|
||||||
role_profile.role_profile = _("{0} User").format(module)
|
"Stock User",
|
||||||
role_profile.append("roles", {"role": module + " User"})
|
"Stock Manager",
|
||||||
role_profile.append("roles", {"role": module + " Manager"})
|
"Item Manager",
|
||||||
role_profile.insert()
|
],
|
||||||
|
"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"]:
|
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
|
||||||
if self.get(fieldname):
|
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)
|
query = query.run(as_dict=True)
|
||||||
|
|
||||||
|
|||||||
@@ -714,6 +714,7 @@ class Item(Document):
|
|||||||
template=self,
|
template=self,
|
||||||
now=frappe.flags.in_test,
|
now=frappe.flags.in_test,
|
||||||
timeout=600,
|
timeout=600,
|
||||||
|
enqueue_after_commit=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_has_variants(self):
|
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', {
|
frappe.ui.form.on('Stock Entry Detail', {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"company",
|
"company",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"posting_time",
|
"posting_time",
|
||||||
|
"column_break_eaoa",
|
||||||
"set_posting_time",
|
"set_posting_time",
|
||||||
"inspection_required",
|
"inspection_required",
|
||||||
"apply_putaway_rule",
|
"apply_putaway_rule",
|
||||||
@@ -640,16 +641,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
|
"depends_on": "eval: doc.fg_completed_qty > 0 && in_list([\"Manufacture\", \"Repack\"], doc.purpose)",
|
||||||
"fieldname": "section_break_7qsm",
|
"fieldname": "section_break_7qsm",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Process Loss"
|
"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",
|
"fieldname": "process_loss_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Process Loss Qty",
|
"label": "Process Loss Qty"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_e92r",
|
"fieldname": "column_break_e92r",
|
||||||
@@ -657,8 +658,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.from_bom && doc.fg_completed_qty",
|
"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",
|
"fieldname": "process_loss_percentage",
|
||||||
"fieldtype": "Percent",
|
"fieldtype": "Percent",
|
||||||
"label": "% Process Loss"
|
"label": "% Process Loss"
|
||||||
@@ -667,6 +666,10 @@
|
|||||||
"fieldname": "items_section",
|
"fieldname": "items_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Items"
|
"label": "Items"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_eaoa",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
@@ -674,7 +677,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-04-06 12:42:56.673180",
|
"modified": "2023-06-09 15:46:28.418339",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry",
|
"name": "Stock Entry",
|
||||||
|
|||||||
@@ -455,13 +455,16 @@ class StockEntry(StockController):
|
|||||||
if self.purpose == "Manufacture" and self.work_order:
|
if self.purpose == "Manufacture" and self.work_order:
|
||||||
for d in self.items:
|
for d in self.items:
|
||||||
if d.is_finished_item:
|
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)
|
item_wise_qty.setdefault(d.item_code, []).append(d.qty)
|
||||||
|
|
||||||
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
||||||
for item_code, qty_list in item_wise_qty.items():
|
for item_code, qty_list in item_wise_qty.items():
|
||||||
total = flt(sum(qty_list), precision)
|
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_qty = flt(self.fg_completed_qty - total, precision)
|
||||||
self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty)
|
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"):
|
for d in prod_order.get("operations"):
|
||||||
total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty)
|
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):
|
if total_completed_qty > flt(completed_qty):
|
||||||
job_card = frappe.db.get_value("Job Card", {"operation_id": d.name}, "name")
|
job_card = frappe.db.get_value("Job Card", {"operation_id": d.name}, "name")
|
||||||
if not job_card:
|
if not job_card:
|
||||||
@@ -1573,16 +1578,36 @@ class StockEntry(StockController):
|
|||||||
if self.purpose not in ("Manufacture", "Repack"):
|
if self.purpose not in ("Manufacture", "Repack"):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.process_loss_qty = 0.0
|
precision = self.precision("process_loss_qty")
|
||||||
if not self.process_loss_percentage:
|
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(
|
self.process_loss_percentage = frappe.get_cached_value(
|
||||||
"BOM", self.bom_no, "process_loss_percentage"
|
"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(
|
self.process_loss_qty = flt(
|
||||||
(flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100
|
(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):
|
def set_work_order_details(self):
|
||||||
if not getattr(self, "pro_doc", None):
|
if not getattr(self, "pro_doc", None):
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class StockSettings(Document):
|
|||||||
frappe.enqueue(
|
frappe.enqueue(
|
||||||
"erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions",
|
"erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions",
|
||||||
now=frappe.flags.in_test,
|
now=frappe.flags.in_test,
|
||||||
|
enqueue_after_commit=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_pending_reposts(self):
|
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
|
range1 = range2 = range3 = above_range3 = 0.0
|
||||||
|
|
||||||
for item in fifo_queue:
|
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
|
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)
|
range1 = flt(range1 + qty, precision)
|
||||||
elif age <= filters.range2:
|
elif age <= flt(filters.range2):
|
||||||
range2 = flt(range2 + qty, precision)
|
range2 = flt(range2 + qty, precision)
|
||||||
elif age <= filters.range3:
|
elif age <= flt(filters.range3):
|
||||||
range3 = flt(range3 + qty, precision)
|
range3 = flt(range3 + qty, precision)
|
||||||
else:
|
else:
|
||||||
above_range3 = flt(above_range3 + qty, precision)
|
above_range3 = flt(above_range3 + qty, precision)
|
||||||
|
|||||||
@@ -803,7 +803,7 @@ class update_entries_after(object):
|
|||||||
|
|
||||||
for item in sr.items:
|
for item in sr.items:
|
||||||
# Skip for Serial and Batch 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
|
continue
|
||||||
|
|
||||||
previous_sle = get_previous_sle(
|
previous_sle = get_previous_sle(
|
||||||
|
|||||||
@@ -7638,20 +7638,19 @@ Restaurant Order Entry Item,Restaurantbestellzugangsposten,
|
|||||||
Served,Serviert,
|
Served,Serviert,
|
||||||
Restaurant Reservation,Restaurant Reservierung,
|
Restaurant Reservation,Restaurant Reservierung,
|
||||||
Waitlisted,Auf der Warteliste,
|
Waitlisted,Auf der Warteliste,
|
||||||
No Show,Keine Show,
|
No Show,Nicht angetreten,
|
||||||
No of People,Nein von Menschen,
|
No of People,Anzahl von Personen,
|
||||||
Reservation Time,Reservierungszeit,
|
Reservation Time,Reservierungszeit,
|
||||||
Reservation End Time,Reservierungsendzeit,
|
Reservation End Time,Reservierungsendzeit,
|
||||||
No of Seats,Anzahl der Sitze,
|
No of Seats,Anzahl der Sitze,
|
||||||
Minimum Seating,Mindestbestuhlung,
|
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.",
|
"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,
|
Campaign Schedules,Kampagnenpläne,
|
||||||
Buyer of Goods and Services.,Käufer von Waren und Dienstleistungen.,
|
Buyer of Goods and Services.,Käufer von Waren und Dienstleistungen.,
|
||||||
CUST-.YYYY.-,CUST-.YYYY.-,
|
|
||||||
Default Company Bank Account,Standard-Bankkonto des Unternehmens,
|
Default Company Bank Account,Standard-Bankkonto des Unternehmens,
|
||||||
From Lead,Aus Lead,
|
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 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,
|
Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein,
|
||||||
Default Price List,Standardpreisliste,
|
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",
|
"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,
|
Parent Item,Übergeordneter Artikel,
|
||||||
List items that form the package.,"Die Artikel auflisten, die das Paket bilden.",
|
List items that form the package.,"Die Artikel auflisten, die das Paket bilden.",
|
||||||
SAL-QTN-.YYYY.-,SAL-QTN-.YYYY.-,
|
|
||||||
Quotation To,Angebot für,
|
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 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",
|
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 Doctype,Zu DocType,
|
||||||
Against Docname,Zu Dokumentenname,
|
Against Docname,Zu Dokumentenname,
|
||||||
Additional Notes,Zusätzliche Bemerkungen,
|
Additional Notes,Zusätzliche Bemerkungen,
|
||||||
SAL-ORD-.YYYY.-,SAL-ORD-.YYYY.-,
|
|
||||||
Skip Delivery Note,Lieferschein überspringen,
|
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.",
|
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,
|
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,
|
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.,
|
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,
|
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,
|
Website Item Group,Webseiten-Artikelgruppe,
|
||||||
Cross Listing of Item in multiple groups,Kreuzweise Auflistung des Artikels in mehreren Gruppen,
|
Cross Listing of Item in multiple groups,Kreuzweise Auflistung des Artikels in mehreren Gruppen,
|
||||||
Default settings for Shopping Cart,Standardeinstellungen für den Warenkorb,
|
Default settings for Shopping Cart,Standardeinstellungen für den Warenkorb,
|
||||||
@@ -8016,7 +8013,6 @@ Contact Information,Kontaktinformationen,
|
|||||||
Email sent to,E-Mail versandt an,
|
Email sent to,E-Mail versandt an,
|
||||||
Dispatch Information,Versandinformationen,
|
Dispatch Information,Versandinformationen,
|
||||||
Estimated Arrival,Voraussichtliche Ankunft,
|
Estimated Arrival,Voraussichtliche Ankunft,
|
||||||
MAT-DT-.YYYY.-,MAT-DT-.YYYY.-,
|
|
||||||
Initial Email Notification Sent,Erste E-Mail-Benachrichtigung gesendet,
|
Initial Email Notification Sent,Erste E-Mail-Benachrichtigung gesendet,
|
||||||
Delivery Details,Lieferdetails,
|
Delivery Details,Lieferdetails,
|
||||||
Driver Email,Fahrer-E-Mail,
|
Driver Email,Fahrer-E-Mail,
|
||||||
@@ -8176,7 +8172,6 @@ Purchase Receipt Item,Kaufbeleg-Artikel,
|
|||||||
Landed Cost Purchase Receipt,Einstandspreis-Kaufbeleg,
|
Landed Cost Purchase Receipt,Einstandspreis-Kaufbeleg,
|
||||||
Landed Cost Taxes and Charges,Einstandspreis Steuern und Gebühren,
|
Landed Cost Taxes and Charges,Einstandspreis Steuern und Gebühren,
|
||||||
Landed Cost Voucher,Beleg über Einstandskosten,
|
Landed Cost Voucher,Beleg über Einstandskosten,
|
||||||
MAT-LCV-.YYYY.-,MAT-LCV-.YYYY.-,
|
|
||||||
Purchase Receipts,Kaufbelege,
|
Purchase Receipts,Kaufbelege,
|
||||||
Purchase Receipt Items,Kaufbeleg-Artikel,
|
Purchase Receipt Items,Kaufbeleg-Artikel,
|
||||||
Get Items From Purchase Receipts,Artikel vom Kaufbeleg übernehmen,
|
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,
|
Landed Cost Help,Hilfe zum Einstandpreis,
|
||||||
Manufacturers used in Items,Hersteller im Artikel verwendet,
|
Manufacturers used in Items,Hersteller im Artikel verwendet,
|
||||||
Limited to 12 characters,Limitiert auf 12 Zeichen,
|
Limited to 12 characters,Limitiert auf 12 Zeichen,
|
||||||
MAT-MR-.YYYY.-,MAT-MR-.YYYY.-,
|
|
||||||
Partially Ordered,Teilweise bestellt,
|
Partially Ordered,Teilweise bestellt,
|
||||||
Transferred,Übergeben,
|
Transferred,Übergeben,
|
||||||
% Ordered,% bestellt,
|
% Ordered,% bestellt,
|
||||||
@@ -8199,7 +8193,6 @@ Prevdoc DocType,Prevdoc DocType,
|
|||||||
Parent Detail docname,Übergeordnetes Detail Dokumentenname,
|
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.",
|
"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)",
|
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.,
|
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),
|
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.,
|
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,
|
Out of AMC,Außerhalb des jährlichen Wartungsvertrags,
|
||||||
Warranty Period (Days),Garantiefrist (Tage),
|
Warranty Period (Days),Garantiefrist (Tage),
|
||||||
Serial No Details,Details zur Seriennummer,
|
Serial No Details,Details zur Seriennummer,
|
||||||
MAT-STE-.YYYY.-,MAT-STE-.JJJJ.-,
|
|
||||||
Stock Entry Type,Bestandsbuchungsart,
|
Stock Entry Type,Bestandsbuchungsart,
|
||||||
Stock Entry (Outward GIT),Bestandsbuchung (Outward GIT),
|
Stock Entry (Outward GIT),Bestandsbuchung (Outward GIT),
|
||||||
Material Consumption for Manufacture,Materialverbrauch für die Herstellung,
|
Material Consumption for Manufacture,Materialverbrauch für die Herstellung,
|
||||||
@@ -8336,7 +8328,6 @@ Stock Queue (FIFO),Lagerverfahren (FIFO),
|
|||||||
Is Cancelled,Ist storniert,
|
Is Cancelled,Ist storniert,
|
||||||
Stock Reconciliation,Bestandsabgleich,
|
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.",
|
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),
|
Reconciliation JSON,Abgleich JSON (JavaScript Object Notation),
|
||||||
Stock Reconciliation Item,Bestandsabgleich-Artikel,
|
Stock Reconciliation Item,Bestandsabgleich-Artikel,
|
||||||
Before reconciliation,Vor Ausgleich,
|
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,
|
Availed ITC Cess,ITC Cess verfügbar,
|
||||||
Is Nil Rated or Exempted,Ist gleich Null oder ausgenommen,
|
Is Nil Rated or Exempted,Ist gleich Null oder ausgenommen,
|
||||||
Is Non GST,Ist nicht GST,
|
Is Non GST,Ist nicht GST,
|
||||||
ACC-SINV-RET-.YYYY.-,ACC-SINV-RET-.YYYY.-,
|
E-Way Bill No.,E-Way Bill Nr.,
|
||||||
E-Way Bill No.,E-Way Bill No.,
|
|
||||||
Is Consolidated,Ist konsolidiert,
|
Is Consolidated,Ist konsolidiert,
|
||||||
Billing Address GSTIN,Rechnungsadresse GSTIN,
|
Billing Address GSTIN,Rechnungsadresse GSTIN,
|
||||||
Customer GSTIN,Kunde GSTIN,
|
Customer GSTIN,Kunde GSTIN,
|
||||||
@@ -9216,7 +9206,7 @@ Id,Ich würde,
|
|||||||
Time Required (In Mins),Erforderliche Zeit (in Minuten),
|
Time Required (In Mins),Erforderliche Zeit (in Minuten),
|
||||||
From Posting Date,Ab dem Buchungsdatum,
|
From Posting Date,Ab dem Buchungsdatum,
|
||||||
To Posting Date,Zum 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,
|
Customer/Lead Name,Name des Kunden / Lead,
|
||||||
Unmarked Days,Nicht markierte Tage,
|
Unmarked Days,Nicht markierte Tage,
|
||||||
Jan,Jan.,
|
Jan,Jan.,
|
||||||
@@ -9275,7 +9265,7 @@ Delay (in Days),Verzögerung (in Tagen),
|
|||||||
Group by Sales Order,Nach Auftrag gruppieren,
|
Group by Sales Order,Nach Auftrag gruppieren,
|
||||||
Sales Value,Verkaufswert,
|
Sales Value,Verkaufswert,
|
||||||
Stock Qty vs Serial No Count,Lagermenge vs Seriennummer,
|
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,
|
Work Order Summary,Arbeitsauftragsübersicht,
|
||||||
Produce Qty,Menge produzieren,
|
Produce Qty,Menge produzieren,
|
||||||
Lead Time (in mins),Vorlaufzeit (in Minuten),
|
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.",
|
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,
|
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.,
|
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,
|
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.,
|
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",
|
"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,
|
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",
|
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,
|
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",
|
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,
|
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,
|
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,
|
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}.,
|
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.,
|
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},
|
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 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},
|
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