mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 04:29:18 +00:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37a7da3371 | ||
|
|
81c362dbe4 | ||
|
|
53034c332b | ||
|
|
0441984405 | ||
|
|
3ba6f40063 | ||
|
|
8e8d0c7bd0 | ||
|
|
f42f1bb35f | ||
|
|
0256c64634 | ||
|
|
e4f583d25a | ||
|
|
7259c0fe30 | ||
|
|
02547115e5 | ||
|
|
0bc9825238 | ||
|
|
bb66126dfa | ||
|
|
0e2abbd08e | ||
|
|
ff78fab176 | ||
|
|
04840762dd | ||
|
|
e7432fc60d | ||
|
|
97f2e88f4c | ||
|
|
37f24ae763 | ||
|
|
25b9127bae | ||
|
|
6e74e6f314 | ||
|
|
240118ee8b | ||
|
|
c1fd95ac66 | ||
|
|
8e340bb7fd | ||
|
|
96a6172999 | ||
|
|
d0d587432d | ||
|
|
07509b5e99 | ||
|
|
99d5b6dc71 | ||
|
|
56b1582027 | ||
|
|
149109649d | ||
|
|
0284328e2c | ||
|
|
a243873ab0 | ||
|
|
197e043fc9 | ||
|
|
925a164101 | ||
|
|
8f1a4b9717 | ||
|
|
67d4020241 | ||
|
|
fe4b2e36cc | ||
|
|
6759b90f85 | ||
|
|
99bc8e849c | ||
|
|
7a25d33547 | ||
|
|
2466e28bf5 | ||
|
|
1096528bb9 | ||
|
|
6ecb064264 | ||
|
|
92300b27c9 | ||
|
|
4fa9626de0 | ||
|
|
9b828b829a | ||
|
|
2936988cc6 | ||
|
|
9fca232578 | ||
|
|
fac22e93d0 | ||
|
|
3109efaf09 | ||
|
|
90ee21f868 | ||
|
|
36c46bb344 | ||
|
|
8c1f6196b8 | ||
|
|
12a31de25a | ||
|
|
3b9400755e | ||
|
|
2ce7300c3c | ||
|
|
b96b3b51b6 | ||
|
|
8f03769bf2 | ||
|
|
d20f3ab492 | ||
|
|
980ca1d8c5 | ||
|
|
4668a2d7d8 | ||
|
|
1a7efbb654 | ||
|
|
ccc2a47e73 | ||
|
|
f98716cc2a | ||
|
|
7903e8d669 | ||
|
|
9a50a0a129 | ||
|
|
1646517dc4 | ||
|
|
38811e792c | ||
|
|
edfb408464 | ||
|
|
ac48c3d4e7 | ||
|
|
1d66b7e5a3 | ||
|
|
ab9bde86f9 | ||
|
|
5c75bb8775 | ||
|
|
115a0123ed | ||
|
|
fdf1dfe46e | ||
|
|
106c154a16 | ||
|
|
00e8b862dd | ||
|
|
4195c50f02 | ||
|
|
fdb8e5b379 | ||
|
|
51cbbee4ca | ||
|
|
5000c09759 | ||
|
|
49e50662b6 | ||
|
|
f2f1f32826 | ||
|
|
fcf6500144 | ||
|
|
f2d5a69af4 | ||
|
|
d5c1c62622 | ||
|
|
e2f8e02c73 | ||
|
|
30cba7ee2c | ||
|
|
21a60c9927 | ||
|
|
0f275a9ff0 | ||
|
|
18402677da | ||
|
|
e9357c193d | ||
|
|
13895fa060 | ||
|
|
64f8498576 | ||
|
|
62ad466a3b | ||
|
|
45899b3017 | ||
|
|
d92a042bf7 | ||
|
|
4d6a71ab4b | ||
|
|
d5fa968078 | ||
|
|
71cbebd31b | ||
|
|
99317768f6 | ||
|
|
9fde7330e0 | ||
|
|
49fb6bec6a | ||
|
|
0f1f5b6f3d | ||
|
|
454e147592 | ||
|
|
701dd9e19b | ||
|
|
2cf82561f6 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.70.8"
|
||||
__version__ = "14.70.13"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -557,7 +557,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:11:04.128015",
|
||||
"modified": "2024-07-18 15:32:29.413598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -74,7 +74,6 @@ class PaymentEntry(AccountsController):
|
||||
self.set_exchange_rate()
|
||||
self.validate_mandatory()
|
||||
self.validate_reference_documents()
|
||||
self.set_tax_withholding()
|
||||
self.set_amounts()
|
||||
self.validate_amounts()
|
||||
self.apply_taxes()
|
||||
@@ -89,6 +88,7 @@ class PaymentEntry(AccountsController):
|
||||
self.validate_allocated_amount()
|
||||
self.validate_paid_invoices()
|
||||
self.ensure_supplier_is_not_blocked()
|
||||
self.set_tax_withholding()
|
||||
self.set_status()
|
||||
|
||||
def on_submit(self):
|
||||
@@ -674,9 +674,7 @@ class PaymentEntry(AccountsController):
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
order_amount = self.get_order_net_total()
|
||||
|
||||
net_total = flt(order_amount) + flt(self.unallocated_amount)
|
||||
net_total = self.calculate_tax_withholding_net_total()
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict(
|
||||
@@ -720,7 +718,26 @@ class PaymentEntry(AccountsController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def get_order_net_total(self):
|
||||
def calculate_tax_withholding_net_total(self):
|
||||
net_total = 0
|
||||
order_details = self.get_order_wise_tax_withholding_net_total()
|
||||
|
||||
for d in self.references:
|
||||
tax_withholding_net_total = order_details.get(d.reference_name)
|
||||
if not tax_withholding_net_total:
|
||||
continue
|
||||
|
||||
net_taxable_outstanding = max(
|
||||
0, d.outstanding_amount - (d.total_amount - tax_withholding_net_total)
|
||||
)
|
||||
|
||||
net_total += min(net_taxable_outstanding, d.allocated_amount)
|
||||
|
||||
net_total += self.unallocated_amount
|
||||
|
||||
return net_total
|
||||
|
||||
def get_order_wise_tax_withholding_net_total(self):
|
||||
if self.party_type == "Supplier":
|
||||
doctype = "Purchase Order"
|
||||
else:
|
||||
@@ -728,12 +745,15 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
||||
|
||||
tax_withholding_net_total = frappe.db.get_value(
|
||||
doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"]
|
||||
return frappe._dict(
|
||||
frappe.db.get_all(
|
||||
doctype,
|
||||
filters={"name": ["in", docnames]},
|
||||
fields=["name", "base_tax_withholding_net_total"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
return tax_withholding_net_total
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
|
||||
@@ -36,7 +36,7 @@ frappe.ui.form.on("Payment Order", {
|
||||
|
||||
// payment Entry
|
||||
if (frm.doc.docstatus === 1 && frm.doc.payment_order_type === "Payment Request") {
|
||||
frm.add_custom_button(__("Create Payment Entries"), function () {
|
||||
frm.add_custom_button(__("Create Journal Entries"), function () {
|
||||
frm.trigger("make_payment_records");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ class PaymentReconciliation(Document):
|
||||
conditions.append(doc.docstatus == 1)
|
||||
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
|
||||
conditions.append(doc.is_return == 1)
|
||||
conditions.append(doc.outstanding_amount != 0)
|
||||
|
||||
if self.payment_name:
|
||||
conditions.append(doc.name.like(f"%{self.payment_name}%"))
|
||||
|
||||
@@ -1335,6 +1335,46 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||
pr.reconcile()
|
||||
|
||||
def test_cr_note_payment_limit_filter(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
for _ in range(6):
|
||||
self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 6)
|
||||
self.assertEqual(len(pr.payments), 6)
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
# Limit should not affect in fetching the unallocated cr_note
|
||||
pr.invoice_limit = 5
|
||||
pr.payment_limit = 5
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -31,6 +31,7 @@ class PricingRule(Document):
|
||||
self.validate_price_list_with_currency()
|
||||
self.validate_dates()
|
||||
self.validate_condition()
|
||||
self.validate_mixed_with_recursion()
|
||||
|
||||
if not self.margin_type:
|
||||
self.margin_rate_or_amount = 0.0
|
||||
@@ -201,6 +202,10 @@ class PricingRule(Document):
|
||||
):
|
||||
frappe.throw(_("Invalid condition expression"))
|
||||
|
||||
def validate_mixed_with_recursion(self):
|
||||
if self.mixed_conditions and self.is_recursive:
|
||||
frappe.throw(_("Recursive Discounts with Mixed condition is not supported by the system"))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1087,6 +1087,18 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
def test_validation_on_mixed_condition_with_recursion(self):
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=10,
|
||||
selling=1,
|
||||
priority=2,
|
||||
min_qty=4,
|
||||
title="_Test Pricing Rule with Min Qty - 2",
|
||||
)
|
||||
pricing_rule.mixed_conditions = True
|
||||
pricing_rule.is_recursive = True
|
||||
self.assertRaises(frappe.ValidationError, pricing_rule.save)
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ class PromotionalScheme(Document):
|
||||
|
||||
self.validate_applicable_for()
|
||||
self.validate_pricing_rules()
|
||||
self.validate_mixed_with_recursion()
|
||||
|
||||
def validate_applicable_for(self):
|
||||
if self.applicable_for:
|
||||
@@ -94,7 +95,7 @@ class PromotionalScheme(Document):
|
||||
docnames = []
|
||||
|
||||
# If user has changed applicable for
|
||||
if self._doc_before_save.applicable_for == self.applicable_for:
|
||||
if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for:
|
||||
return
|
||||
|
||||
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
|
||||
@@ -108,6 +109,7 @@ class PromotionalScheme(Document):
|
||||
frappe.delete_doc("Pricing Rule", docname.name)
|
||||
|
||||
def on_update(self):
|
||||
self.validate()
|
||||
pricing_rules = (
|
||||
frappe.get_all(
|
||||
"Pricing Rule",
|
||||
@@ -119,6 +121,15 @@ class PromotionalScheme(Document):
|
||||
)
|
||||
self.update_pricing_rules(pricing_rules)
|
||||
|
||||
def validate_mixed_with_recursion(self):
|
||||
if self.mixed_conditions:
|
||||
if self.product_discount_slabs:
|
||||
for slab in self.product_discount_slabs:
|
||||
if slab.is_recursive:
|
||||
frappe.throw(
|
||||
_("Recursive Discounts with Mixed condition is not supported by the system")
|
||||
)
|
||||
|
||||
def update_pricing_rules(self, pricing_rules):
|
||||
rules = {}
|
||||
count = 0
|
||||
|
||||
@@ -107,6 +107,25 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_validation_on_recurse_with_mixed_condition(self):
|
||||
ps = make_promotional_scheme()
|
||||
ps.set("price_discount_slabs", [])
|
||||
ps.set(
|
||||
"product_discount_slabs",
|
||||
[
|
||||
{
|
||||
"rule_description": "12+1",
|
||||
"min_qty": 12,
|
||||
"free_item": "_Test Item 2",
|
||||
"free_qty": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 12,
|
||||
}
|
||||
],
|
||||
)
|
||||
ps.mixed_conditions = True
|
||||
self.assertRaises(frappe.ValidationError, ps.save)
|
||||
|
||||
|
||||
def make_promotional_scheme(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -59,25 +59,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.show_stock_ledger();
|
||||
}
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
|
||||
this.frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update."));
|
||||
this.frm.add_custom_button(__('Repost Accounting Entries'),
|
||||
() => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'repost_accounting_entries',
|
||||
freeze: true,
|
||||
freeze_message: __('Reposting...'),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__('Accounting Entries are reposted.'));
|
||||
me.frm.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).removeClass('btn-default').addClass('btn-warning');
|
||||
}
|
||||
|
||||
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
|
||||
if(doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
|
||||
@@ -170,7 +170,6 @@
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"repost_required",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
@@ -361,7 +360,8 @@
|
||||
"description": "Once set, this invoice will be on hold till the set date",
|
||||
"fieldname": "release_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Release Date"
|
||||
"label": "Release Date",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cb_17",
|
||||
@@ -1590,15 +1590,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company Default Round Off Cost Center"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_transaction_date_exchange_rate",
|
||||
@@ -1619,7 +1610,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-20 15:57:00.736868",
|
||||
"modified": "2024-07-25 19:42:36.931278",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -590,17 +590,17 @@ class PurchaseInvoice(BuyingController):
|
||||
self.process_common_party_accounting()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||
@@ -1498,6 +1498,9 @@ class PurchaseInvoice(BuyingController):
|
||||
self.db_set("release_date", None)
|
||||
|
||||
def set_tax_withholding(self):
|
||||
self.set("advance_tax", [])
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
@@ -1539,8 +1542,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.remove(d)
|
||||
|
||||
## Add pending vouchers on which tax was withheld
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
for voucher_no, voucher_details in voucher_wise_amount.items():
|
||||
self.append(
|
||||
"tax_withheld_vouchers",
|
||||
@@ -1555,7 +1556,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
|
||||
self.set("advance_tax", [])
|
||||
for tax in advance_taxes:
|
||||
allocated_amount = 0
|
||||
pending_amount = flt(tax.tax_amount - tax.allocated_amount)
|
||||
|
||||
@@ -1908,18 +1908,15 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
pi.items[0].expense_account = "Service - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
pi.save()
|
||||
pi.load_from_db()
|
||||
self.assertTrue(pi.repost_required)
|
||||
pi.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||
["Service - _TC", 1000, 0.0, nowdate()],
|
||||
]
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
pi.load_from_db()
|
||||
self.assertFalse(pi.repost_required)
|
||||
|
||||
def test_default_cost_center_for_purchase(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
|
||||
@@ -49,25 +49,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
|
||||
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
|
||||
this.frm.set_intro(__("Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update."));
|
||||
this.frm.add_custom_button(__('Repost Accounting Entries'),
|
||||
() => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'repost_accounting_entries',
|
||||
freeze: true,
|
||||
freeze_message: __('Reposting...'),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__('Accounting Entries are reposted'));
|
||||
me.frm.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).removeClass('btn-default').addClass('btn-warning');
|
||||
}
|
||||
|
||||
if (this.frm.doc.is_return) {
|
||||
this.frm.return_print_format = "Sales Invoice Return";
|
||||
}
|
||||
@@ -428,12 +409,16 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
} else {
|
||||
var me = this;
|
||||
const for_validate = me.frm.doc.is_return ? true : false;
|
||||
return this.frm.call({
|
||||
doc: me.frm.doc,
|
||||
method: "set_missing_values",
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if(r.message && r.message.print_format) {
|
||||
args: {
|
||||
for_validate: for_validate,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message && r.message.print_format) {
|
||||
me.frm.pos_print_format = r.message.print_format;
|
||||
}
|
||||
me.frm.trigger("update_stock");
|
||||
|
||||
@@ -213,7 +213,6 @@
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"remarks",
|
||||
"repost_required",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -2184,7 +2183,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-08 18:02:28.549041",
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -385,7 +385,6 @@ class SalesInvoice(SellingController):
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
self.db_set("repost_required", 0)
|
||||
|
||||
if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
|
||||
update_company_current_month_sales(self.company)
|
||||
@@ -532,23 +531,23 @@ class SalesInvoice(SellingController):
|
||||
data.sales_invoice = sales_invoice
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def set_paid_amount(self):
|
||||
paid_amount = 0.0
|
||||
|
||||
@@ -2884,13 +2884,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.items[0].income_account = "Service - _TC"
|
||||
si.additional_discount_account = "_Test Account Sales - _TC"
|
||||
si.taxes[0].account_head = "TDS Payable - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
si.save()
|
||||
|
||||
si.load_from_db()
|
||||
self.assertTrue(si.repost_required)
|
||||
|
||||
si.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Sales - _TC", 22.0, 0.0, nowdate()],
|
||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||
@@ -2900,9 +2896,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
si.load_from_db()
|
||||
self.assertFalse(si.repost_required)
|
||||
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
|
||||
@@ -236,6 +236,11 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
payment_entry_vouchers = get_payment_entry_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
advance_vouchers = get_advance_vouchers(
|
||||
parties,
|
||||
company=inv.company,
|
||||
@@ -243,7 +248,8 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
to_date=tax_details.to_date,
|
||||
party_type=party_type,
|
||||
)
|
||||
taxable_vouchers = vouchers + advance_vouchers
|
||||
|
||||
taxable_vouchers = vouchers + advance_vouchers + payment_entry_vouchers
|
||||
tax_deducted_on_advances = 0
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
@@ -355,6 +361,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
return vouchers, voucher_wise_amount
|
||||
|
||||
|
||||
def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
payment_entry_filters = {
|
||||
"party_type": party_type,
|
||||
"party": ("in", parties),
|
||||
"docstatus": 1,
|
||||
"apply_tax_withholding_amount": 1,
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
return frappe.db.get_all("Payment Entry", filters=payment_entry_filters, pluck="name")
|
||||
|
||||
|
||||
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type="Supplier"):
|
||||
"""
|
||||
Use Payment Ledger to fetch unallocated Advance Payments
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -139,6 +139,7 @@ class ReceivablePayableReport:
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
self.get_invoices(ple)
|
||||
|
||||
@@ -253,7 +254,7 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if ple.cost_center:
|
||||
if not row.cost_center and ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
@@ -288,13 +289,13 @@ class ReceivablePayableReport:
|
||||
|
||||
must_consider = False
|
||||
if self.filters.get("for_revaluation_journals"):
|
||||
if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision
|
||||
if (abs(row.outstanding) >= 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) >= 0.0 / 10**self.currency_precision
|
||||
):
|
||||
must_consider = True
|
||||
else:
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
if (abs(row.outstanding) >= 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) >= 1.0 / 10**self.currency_precision)
|
||||
or (row.voucher_no in self.err_journals)
|
||||
):
|
||||
must_consider = True
|
||||
|
||||
@@ -53,11 +53,13 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def create_payment_entry(self, docname):
|
||||
def create_payment_entry(self, docname, do_not_submit=False):
|
||||
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
if not do_not_submit:
|
||||
pe.submit()
|
||||
return pe
|
||||
|
||||
def create_credit_note(self, docname, do_not_submit=False):
|
||||
credit_note = create_sales_invoice(
|
||||
@@ -955,3 +957,69 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
def test_accounts_receivable_output_for_minor_outstanding(self):
|
||||
"""
|
||||
AR/AP should report miniscule outstanding of 0.01. Or else there will be slight difference with General Ledger/Trial Balance
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account=self.cash, party_amount=99.99)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [100, 100, 99.99, 0.01]
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment,
|
||||
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||
)
|
||||
|
||||
def test_cost_center_on_report_output(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.cost_center = self.cost_center
|
||||
si.save().submit()
|
||||
|
||||
new_cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "East Wing",
|
||||
"parent_cost_center": self.company + " - " + self.company_abbr,
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
new_cc.save()
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
pe = self.create_payment_entry(si.name, do_not_submit=True)
|
||||
pe.cost_center = new_cc.name
|
||||
pe.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [si.name, si.cost_center, 60]
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
|
||||
|
||||
@@ -694,7 +694,8 @@ class GrossProfitGenerator:
|
||||
|
||||
def get_average_buying_rate(self, row, item_code):
|
||||
args = row
|
||||
if item_code not in self.average_buying_rate:
|
||||
key = (item_code, row.warehouse)
|
||||
if key not in self.average_buying_rate:
|
||||
args.update(
|
||||
{
|
||||
"voucher_type": row.parenttype,
|
||||
@@ -705,9 +706,9 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
average_buying_rate = get_incoming_rate(args)
|
||||
self.average_buying_rate[item_code] = flt(average_buying_rate)
|
||||
self.average_buying_rate[key] = flt(average_buying_rate)
|
||||
|
||||
return self.average_buying_rate[item_code]
|
||||
return self.average_buying_rate[key]
|
||||
|
||||
def get_last_purchase_rate(self, item_code, row):
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
@@ -46,7 +46,7 @@ frappe.query_reports["Item-wise Purchase Register"] = {
|
||||
label: __("Group By"),
|
||||
fieldname: "group_by",
|
||||
fieldtype: "Select",
|
||||
options: ["Supplier", "Item Group", "Item", "Invoice"],
|
||||
options: ["", "Supplier", "Item Group", "Item", "Invoice"],
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -306,14 +306,15 @@ def apply_conditions(query, pi, pii, filters):
|
||||
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||
query = query.orderby(pii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(filters, "Purchase Invoice")
|
||||
query = apply_group_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_items(filters, additional_table_columns):
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
pii = frappe.qb.DocType("Purchase Invoice Item")
|
||||
doctype = "Purchase Invoice"
|
||||
pi = frappe.qb.DocType(doctype)
|
||||
pii = frappe.qb.DocType(f"{doctype} Item")
|
||||
Item = frappe.qb.DocType("Item")
|
||||
query = (
|
||||
frappe.qb.from_(pi)
|
||||
@@ -350,6 +351,7 @@ def get_items(filters, additional_table_columns):
|
||||
pi.mode_of_payment,
|
||||
)
|
||||
.where(pi.docstatus == 1)
|
||||
.where(pii.parenttype == doctype)
|
||||
)
|
||||
|
||||
if filters.get("supplier"):
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.report.item_wise_purchase_register.item_wise_purchase_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestItemWisePurchaseRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
pi = make_purchase_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
do_not_save=1,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
qty=1,
|
||||
)
|
||||
|
||||
pi = pi.save()
|
||||
if not do_not_submit:
|
||||
pi = pi.submit()
|
||||
return pi
|
||||
|
||||
def test_basic_report_output(self):
|
||||
pi = self.create_purchase_invoice()
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
|
||||
expected_result = {
|
||||
"item_code": pi.items[0].item_code,
|
||||
"invoice": pi.name,
|
||||
"posting_date": getdate(),
|
||||
"supplier": pi.supplier,
|
||||
"credit_to": pi.credit_to,
|
||||
"company": self.company,
|
||||
"expense_account": pi.items[0].expense_account,
|
||||
"stock_qty": 1.0,
|
||||
"stock_uom": pi.items[0].stock_uom,
|
||||
"rate": 100.0,
|
||||
"amount": 100.0,
|
||||
"total_tax": 0,
|
||||
"total": 100.0,
|
||||
"currency": "INR",
|
||||
}
|
||||
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
@@ -64,7 +64,7 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
||||
label: __("Group By"),
|
||||
fieldname: "group_by",
|
||||
fieldtype: "Select",
|
||||
options: ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"],
|
||||
options: ["", "Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"],
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -407,8 +407,9 @@ def apply_group_by_conditions(query, si, ii, filters):
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sii = frappe.qb.DocType("Sales Invoice Item")
|
||||
doctype = "Sales Invoice"
|
||||
si = frappe.qb.DocType(doctype)
|
||||
sii = frappe.qb.DocType(f"{doctype} Item")
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
@@ -456,6 +457,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
sii.qty,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sii.parenttype == doctype)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
)
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def test_basic_report_output(self):
|
||||
si = self.create_sales_invoice()
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
|
||||
expected_result = {
|
||||
"item_code": si.items[0].item_code,
|
||||
"invoice": si.name,
|
||||
"posting_date": getdate(),
|
||||
"customer": si.customer,
|
||||
"debit_to": si.debit_to,
|
||||
"company": self.company,
|
||||
"income_account": si.items[0].income_account,
|
||||
"stock_qty": 1.0,
|
||||
"stock_uom": si.items[0].stock_uom,
|
||||
"rate": 100.0,
|
||||
"amount": 100.0,
|
||||
"total_tax": 0,
|
||||
"total_other_charges": 0,
|
||||
"total": 100.0,
|
||||
"currency": "INR",
|
||||
}
|
||||
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
@@ -12,7 +12,7 @@ def execute(filters=None):
|
||||
else:
|
||||
party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
|
||||
filters.update({"naming_series": party_naming_by})
|
||||
filters["naming_series"] = party_naming_by
|
||||
|
||||
validate_filters(filters)
|
||||
(
|
||||
@@ -63,21 +63,23 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
tax_withholding_category = tds_accounts.get(entry.account)
|
||||
# or else the consolidated value from the voucher document
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = tax_category_map.get(name)
|
||||
tax_withholding_category = tax_category_map.get((voucher_type, name))
|
||||
# or else from the party default
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
if net_total_map.get(name):
|
||||
if net_total_map.get((voucher_type, name)):
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
if rate:
|
||||
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||
elif voucher_type == "Purchase Invoice":
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(name)
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
|
||||
(voucher_type, name)
|
||||
)
|
||||
else:
|
||||
total_amount, grand_total, base_total = net_total_map.get(name)
|
||||
total_amount, grand_total, base_total = net_total_map.get((voucher_type, name))
|
||||
else:
|
||||
total_amount += entry.credit
|
||||
|
||||
@@ -97,7 +99,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
}
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
row.update({"party_name": party_map.get(party, {}).get(party_name)})
|
||||
row["party_name"] = party_map.get(party, {}).get(party_name)
|
||||
|
||||
row.update(
|
||||
{
|
||||
@@ -279,7 +281,6 @@ def get_tds_docs(filters):
|
||||
journal_entries = []
|
||||
tax_category_map = frappe._dict()
|
||||
net_total_map = frappe._dict()
|
||||
frappe._dict()
|
||||
journal_entry_party_map = frappe._dict()
|
||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
||||
|
||||
@@ -412,7 +413,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
)
|
||||
|
||||
for entry in entries:
|
||||
tax_category_map.update({entry.name: entry.tax_withholding_category})
|
||||
tax_category_map[(doctype, entry.name)] = entry.tax_withholding_category
|
||||
if doctype == "Purchase Invoice":
|
||||
value = [
|
||||
entry.base_tax_withholding_net_total,
|
||||
@@ -427,7 +428,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
|
||||
else:
|
||||
value = [entry.total_amount] * 3
|
||||
net_total_map.update({entry.name: value})
|
||||
|
||||
net_total_map[(doctype, entry.name)] = value
|
||||
|
||||
|
||||
def get_tax_rate_map(filters):
|
||||
|
||||
@@ -1571,6 +1571,18 @@ def auto_create_exchange_rate_revaluation_weekly() -> None:
|
||||
create_err_and_its_journals(companies)
|
||||
|
||||
|
||||
def auto_create_exchange_rate_revaluation_monthly() -> None:
|
||||
"""
|
||||
Executed by background job
|
||||
"""
|
||||
companies = frappe.db.get_all(
|
||||
"Company",
|
||||
filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": "Montly"},
|
||||
fields=["name", "submit_err_jv"],
|
||||
)
|
||||
create_err_and_its_journals(companies)
|
||||
|
||||
|
||||
def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
ple_map = []
|
||||
if gl_entries:
|
||||
|
||||
@@ -1689,12 +1689,12 @@ def create_asset(**args):
|
||||
return asset
|
||||
|
||||
|
||||
def create_asset_category():
|
||||
def create_asset_category(enable_cwip=1):
|
||||
asset_category = frappe.new_doc("Asset Category")
|
||||
asset_category.asset_category_name = "Computers"
|
||||
asset_category.total_number_of_depreciations = 3
|
||||
asset_category.frequency_of_depreciation = 3
|
||||
asset_category.enable_cwip_accounting = 1
|
||||
asset_category.enable_cwip_accounting = enable_cwip
|
||||
asset_category.append(
|
||||
"accounts",
|
||||
{
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
"fieldname": "supplier_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Supplier Type",
|
||||
"options": "Company\nIndividual\nProprietorship\nPartnership",
|
||||
"options": "Company\nIndividual\nPartnership",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -43,9 +43,10 @@ def get_data(filters):
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(po)
|
||||
.from_(po_item)
|
||||
.inner_join(po_item)
|
||||
.on(po_item.parent == po.name)
|
||||
.left_join(pi_item)
|
||||
.on(pi_item.po_detail == po_item.name)
|
||||
.on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1))
|
||||
.select(
|
||||
po.transaction_date.as_("date"),
|
||||
po_item.schedule_date.as_("required_date"),
|
||||
|
||||
@@ -742,6 +742,9 @@ class AccountsController(TransactionBase):
|
||||
# reset pricing rule fields if pricing_rule_removed
|
||||
item.set(fieldname, value)
|
||||
|
||||
elif fieldname == "expense_account" and not item.get("expense_account"):
|
||||
item.expense_account = value
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
|
||||
"is_fixed_asset"
|
||||
):
|
||||
@@ -2373,16 +2376,12 @@ class AccountsController(TransactionBase):
|
||||
|
||||
@frappe.whitelist()
|
||||
def repost_accounting_entries(self):
|
||||
if self.repost_required:
|
||||
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_ledger.company = self.company
|
||||
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
|
||||
repost_ledger.flags.ignore_permissions = True
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
self.db_set("repost_required", 0)
|
||||
else:
|
||||
frappe.throw(_("No updates pending for reposting"))
|
||||
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_ledger.company = self.company
|
||||
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
|
||||
repost_ledger.flags.ignore_permissions = True
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -31,7 +31,7 @@ class SellingController(StockController):
|
||||
def validate(self):
|
||||
super().validate()
|
||||
self.validate_items()
|
||||
if not self.get("is_debit_note"):
|
||||
if not (self.get("is_debit_note") or self.get("is_return")):
|
||||
self.validate_max_discount()
|
||||
self.validate_selling_price()
|
||||
self.set_qty_as_per_stock_uom()
|
||||
|
||||
@@ -463,6 +463,7 @@ scheduler_events = {
|
||||
"monthly_long": [
|
||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans",
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -271,6 +271,7 @@ erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
|
||||
erpnext.patches.v14_0.france_depreciation_warning
|
||||
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
||||
erpnext.patches.v14_0.update_proprietorship_to_individual
|
||||
|
||||
[post_model_sync]
|
||||
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
for doctype in ["Customer", "Supplier"]:
|
||||
field = doctype.lower() + "_type"
|
||||
frappe.db.set_value(doctype, {field: "Proprietorship"}, field, "Individual")
|
||||
@@ -1614,12 +1614,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
apply_product_discount(args) {
|
||||
const items = this.frm.doc.items.filter(d => (d.is_free_item)) || [];
|
||||
|
||||
const exist_items = items.map(row => (row.item_code, row.pricing_rules));
|
||||
const exist_items = items.map(row => { return {item_code: row.item_code, pricing_rules: row.pricing_rules};});
|
||||
|
||||
args.free_item_data.forEach(pr_row => {
|
||||
let row_to_modify = {};
|
||||
if (!items || !in_list(exist_items, (pr_row.item_code, pr_row.pricing_rules))) {
|
||||
|
||||
// If there are no free items, or if the current free item doesn't exist in the table, add it
|
||||
if (!items || !exist_items.filter(e_row => {
|
||||
return e_row.item_code == pr_row.item_code && e_row.pricing_rules == pr_row.pricing_rules;
|
||||
}).length) {
|
||||
row_to_modify = frappe.model.add_child(this.frm.doc,
|
||||
this.frm.doc.doctype + ' Item', 'items');
|
||||
|
||||
@@ -1642,6 +1645,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
apply_price_list(item, reset_plc_conversion) {
|
||||
// We need to reset plc_conversion_rate sometimes because the call to
|
||||
// `erpnext.stock.get_item_details.apply_price_list` is sensitive to its value
|
||||
|
||||
|
||||
if (this.frm.doc.doctype === "Material Request") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reset_plc_conversion) {
|
||||
this.frm.set_value("plc_conversion_rate", "");
|
||||
}
|
||||
@@ -1657,7 +1666,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
me.in_apply_price_list = true;
|
||||
return this.frm.call({
|
||||
method: "erpnext.stock.get_item_details.apply_price_list",
|
||||
args: { args: args },
|
||||
args: { args: args, doc: me.frm.doc },
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
frappe.run_serially([
|
||||
|
||||
@@ -36,7 +36,6 @@ frappe.ui.form.on("Import Supplier Invoice", {
|
||||
toggle_read_only_fields: function (frm) {
|
||||
if (["File Import Completed", "Processing File Data"].includes(frm.doc.status)) {
|
||||
cur_frm.set_read_only();
|
||||
cur_frm.refresh_fields();
|
||||
frm.set_df_property("import_invoices", "hidden", 1);
|
||||
} else {
|
||||
frm.set_df_property("import_invoices", "hidden", 0);
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"label": "Customer Type",
|
||||
"oldfieldname": "customer_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Company\nIndividual\nProprietorship\nPartnership",
|
||||
"options": "Company\nIndividual\nPartnership",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -698,7 +698,7 @@
|
||||
"fieldname": "auto_err_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Frequency",
|
||||
"options": "Daily\nWeekly"
|
||||
"options": "Daily\nWeekly\nMonthly"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -712,7 +712,7 @@
|
||||
"image_field": "company_logo",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-27 17:32:49.057386",
|
||||
"modified": "2024-07-24 18:17:56.413971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
|
||||
@@ -19,6 +19,7 @@ class HolidayList(Document):
|
||||
def validate(self):
|
||||
self.validate_days()
|
||||
self.total_holidays = len(self.holidays)
|
||||
self.sort_holidays()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_weekly_off_dates(self):
|
||||
@@ -33,8 +34,6 @@ class HolidayList(Document):
|
||||
|
||||
self.append("holidays", {"description": _(self.weekly_off), "holiday_date": d, "weekly_off": 1})
|
||||
|
||||
self.sort_holidays()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_supported_countries(self):
|
||||
from holidays.utils import list_supported_countries
|
||||
@@ -76,8 +75,6 @@ class HolidayList(Document):
|
||||
"holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0}
|
||||
)
|
||||
|
||||
self.sort_holidays()
|
||||
|
||||
def sort_holidays(self):
|
||||
self.holidays.sort(key=lambda x: getdate(x.holiday_date))
|
||||
for i in range(len(self.holidays)):
|
||||
|
||||
@@ -162,7 +162,7 @@ def make_taxes_and_charges_template(company_name, doctype, template):
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
return doc
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ def make_item_tax_template(company_name, template):
|
||||
# Ingone validations to make doctypes faster
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
return doc
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ def get_or_create_account(company_name, account):
|
||||
doc = frappe.get_doc(account)
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
doc.insert(ignore_permissions=True, ignore_mandatory=True, ignore_if_duplicate=True)
|
||||
return doc
|
||||
|
||||
|
||||
|
||||
@@ -156,6 +156,33 @@ class TestItem(FrappeTestCase):
|
||||
for key, value in to_check.items():
|
||||
self.assertEqual(value, details.get(key), key)
|
||||
|
||||
def test_get_asset_item_details(self):
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
|
||||
|
||||
create_asset_category(0)
|
||||
create_fixed_asset_item()
|
||||
|
||||
details = get_item_details(
|
||||
{
|
||||
"item_code": "Macbook Pro",
|
||||
"company": "_Test Company",
|
||||
"currency": "INR",
|
||||
"doctype": "Purchase Receipt",
|
||||
}
|
||||
)
|
||||
self.assertEqual(details.get("expense_account"), "_Test Fixed Asset - _TC")
|
||||
|
||||
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", "1")
|
||||
details = get_item_details(
|
||||
{
|
||||
"item_code": "Macbook Pro",
|
||||
"company": "_Test Company",
|
||||
"currency": "INR",
|
||||
"doctype": "Purchase Receipt",
|
||||
}
|
||||
)
|
||||
self.assertEqual(details.get("expense_account"), "CWIP Account - _TC")
|
||||
|
||||
def test_item_tax_template(self):
|
||||
expected_item_tax_template = [
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import OrderedDict, defaultdict
|
||||
from itertools import groupby
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc
|
||||
from frappe.query_builder import Case
|
||||
@@ -26,6 +26,9 @@ from erpnext.stock.get_item_details import get_conversion_factor
|
||||
class PickList(Document):
|
||||
def validate(self):
|
||||
self.validate_for_qty()
|
||||
if self.pick_manually and self.get("locations"):
|
||||
self.validate_stock_qty()
|
||||
self.check_serial_no_status()
|
||||
|
||||
def before_save(self):
|
||||
self.update_status()
|
||||
@@ -35,6 +38,60 @@ class PickList(Document):
|
||||
if self.get("locations"):
|
||||
self.validate_sales_order_percentage()
|
||||
|
||||
def validate_stock_qty(self):
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
for row in self.get("locations"):
|
||||
if row.batch_no and not row.qty:
|
||||
batch_qty = get_batch_qty(row.batch_no, row.warehouse, row.item_code)
|
||||
|
||||
if row.qty > batch_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}."
|
||||
).format(row.idx, row.item_code, batch_qty, row.batch_no, bold(row.warehouse)),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
bin_qty = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": row.item_code, "warehouse": row.warehouse},
|
||||
"actual_qty",
|
||||
)
|
||||
|
||||
if row.qty > bin_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}."
|
||||
).format(row.idx, row.qty, bold(row.item_code), bin_qty, bold(row.warehouse)),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
def check_serial_no_status(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
for row in self.get("locations"):
|
||||
if not row.serial_no:
|
||||
continue
|
||||
|
||||
picked_serial_nos = get_serial_nos(row.serial_no)
|
||||
validated_serial_nos = frappe.get_all(
|
||||
"Serial No",
|
||||
filters={"name": ("in", picked_serial_nos), "warehouse": row.warehouse},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
incorrect_serial_nos = set(picked_serial_nos) - set(validated_serial_nos)
|
||||
if incorrect_serial_nos:
|
||||
frappe.throw(
|
||||
_("The Serial No at Row #{0}: {1} is not available in warehouse {2}.").format(
|
||||
row.idx, ", ".join(incorrect_serial_nos), row.warehouse
|
||||
),
|
||||
title=_("Incorrect Warehouse"),
|
||||
)
|
||||
|
||||
def validate_sales_order_percentage(self):
|
||||
# set percentage picked in SO
|
||||
for location in self.get("locations"):
|
||||
|
||||
@@ -802,3 +802,45 @@ class TestPickList(FrappeTestCase):
|
||||
a = set(picked_serial_no)
|
||||
b = set([x for x in location.serial_no.split("\n") if x])
|
||||
self.assertSetEqual(b, b.difference(a))
|
||||
|
||||
def test_validate_picked_qty_with_manual_option(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
non_serialized_item = make_item(
|
||||
"Test Non Serialized Pick List Item For Manual Option", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
serialized_item = make_item(
|
||||
"Test Serialized Pick List Item For Manual Option",
|
||||
properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-HSNMSPLI-.####"},
|
||||
).name
|
||||
|
||||
batched_item = make_item(
|
||||
"Test Batched Pick List Item For Manual Option",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "SN-HBNMSPLI-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=non_serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100)
|
||||
make_stock_entry(item=serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100)
|
||||
make_stock_entry(item=batched_item, to_warehouse=warehouse, qty=10, basic_rate=100)
|
||||
|
||||
so = make_sales_order(
|
||||
item_code=non_serialized_item, qty=10, rate=100, do_not_save=True, warehouse=warehouse
|
||||
)
|
||||
so.append("items", {"item_code": serialized_item, "qty": 10, "rate": 100, "warehouse": warehouse})
|
||||
so.append("items", {"item_code": batched_item, "qty": 10, "rate": 100, "warehouse": warehouse})
|
||||
so.set_missing_values()
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.pick_manually = 1
|
||||
|
||||
for row in pl.locations:
|
||||
row.qty = row.qty + 10
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pl.save)
|
||||
|
||||
@@ -558,15 +558,7 @@ class PurchaseReceipt(BuyingController):
|
||||
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
||||
|
||||
if d.is_fixed_asset:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(d.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
|
||||
stock_asset_account_name = get_asset_account(
|
||||
account_type, asset_category=d.asset_category, company=self.company
|
||||
)
|
||||
stock_asset_account_name = d.expense_account
|
||||
|
||||
stock_value_diff = (
|
||||
flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount)
|
||||
@@ -670,7 +662,6 @@ class PurchaseReceipt(BuyingController):
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False):
|
||||
negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")])
|
||||
is_asset_pr = any(d.is_fixed_asset for d in self.get("items"))
|
||||
# Cost center-wise amount breakup for other charges included for valuation
|
||||
valuation_tax = {}
|
||||
for tax in self.get("taxes"):
|
||||
@@ -695,26 +686,10 @@ class PurchaseReceipt(BuyingController):
|
||||
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
|
||||
total_valuation_amount = sum(valuation_tax.values())
|
||||
amount_including_divisional_loss = negative_expense_to_be_booked
|
||||
stock_rbnb = (
|
||||
self.get("asset_received_but_not_billed")
|
||||
if is_asset_pr
|
||||
else self.get_company_default("stock_received_but_not_billed")
|
||||
)
|
||||
i = 1
|
||||
for tax in self.get("taxes"):
|
||||
if valuation_tax.get(tax.name):
|
||||
if via_landed_cost_voucher or self.is_landed_cost_booked_for_any_item():
|
||||
account = tax.account_head
|
||||
else:
|
||||
negative_expense_booked_in_pi = frappe.db.sql(
|
||||
"""select name from `tabPurchase Invoice Item` pi
|
||||
where docstatus = 1 and purchase_receipt=%s
|
||||
and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice'
|
||||
and voucher_no=pi.parent and account=%s)""",
|
||||
(self.name, tax.account_head),
|
||||
)
|
||||
account = stock_rbnb if negative_expense_booked_in_pi else tax.account_head
|
||||
|
||||
account = tax.account_head
|
||||
if i == len(valuation_tax):
|
||||
applicable_amount = amount_including_divisional_loss
|
||||
else:
|
||||
|
||||
@@ -2499,6 +2499,110 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
lcv.save().submit()
|
||||
return lcv
|
||||
|
||||
def test_tax_account_heads_on_item_repost_without_lcv(self):
|
||||
"""
|
||||
PO -> PR -> PI
|
||||
Backdated `Repost Item valuation` should not merge tax account heads into stock_rbnb if Purchase Receipt was created first
|
||||
This scenario is without LCV
|
||||
"""
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_purchase_order,
|
||||
make_pr_against_po,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
|
||||
|
||||
stock_rbnb = "Stock Received But Not Billed - _TC"
|
||||
stock_in_hand = "Stock In Hand - _TC"
|
||||
test_cc = "_Test Cost Center - _TC"
|
||||
test_company = "_Test Company"
|
||||
creditors = "Creditors - _TC"
|
||||
|
||||
company_doc = frappe.get_doc("Company", test_company)
|
||||
company_doc.enable_perpetual_inventory = True
|
||||
company_doc.stock_received_but_not_billed = stock_rbnb
|
||||
company_doc.default_inventory_account = stock_in_hand
|
||||
company_doc.save()
|
||||
|
||||
packaging_charges_account = create_account(
|
||||
account_name="Packaging Charges",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
company=test_company,
|
||||
account_type="Tax",
|
||||
)
|
||||
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||
po.taxes = []
|
||||
po.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Valuation and Total",
|
||||
"account_head": packaging_charges_account,
|
||||
"cost_center": test_cc,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
"charge_type": "Actual",
|
||||
"tax_amount": 250,
|
||||
},
|
||||
)
|
||||
po.save().submit()
|
||||
|
||||
pr = make_pr_against_po(po.name, received_qty=10)
|
||||
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles = [
|
||||
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(expected_pr_gles, pr_gl_entries)
|
||||
|
||||
# Make PI against Purchase Receipt
|
||||
pi = make_purchase_invoice(pr.name).save().submit()
|
||||
pi_gl_entries = get_gl_entries(pi.doctype, pi.name, skip_cancelled=True)
|
||||
expected_pi_gles = [
|
||||
{"account": stock_rbnb, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": packaging_charges_account, "debit": 250.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": creditors, "debit": 0.0, "credit": 1250.0, "cost_center": None},
|
||||
]
|
||||
self.assertEqual(expected_pi_gles, pi_gl_entries)
|
||||
|
||||
# Trigger Repost Item Valudation on a older date
|
||||
repost_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"based_on": "Item and Warehouse",
|
||||
"item_code": pr.items[0].item_code,
|
||||
"warehouse": pr.items[0].warehouse,
|
||||
"posting_date": add_days(pr.posting_date, -1),
|
||||
"posting_time": "00:00:00",
|
||||
"company": pr.company,
|
||||
"allow_negative_stock": 1,
|
||||
"via_landed_cost_voucher": 0,
|
||||
"allow_zero_rate": 0,
|
||||
}
|
||||
)
|
||||
repost_doc.save().submit()
|
||||
|
||||
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles_after_repost = [
|
||||
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
|
||||
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
|
||||
|
||||
# teardown
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
|
||||
company_doc.enable_perpetual_inventory = False
|
||||
company_doc.stock_received_but_not_billed = None
|
||||
company_doc.default_inventory_account = None
|
||||
company_doc.save()
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -237,9 +237,23 @@ def repost(doc):
|
||||
doc.log_error("Unable to repost item valuation")
|
||||
|
||||
message = frappe.message_log.pop() if frappe.message_log else ""
|
||||
|
||||
status = "Failed"
|
||||
# If failed because of timeout, set status to In Progress
|
||||
if traceback and "timeout" in traceback.lower():
|
||||
status = "In Progress"
|
||||
|
||||
if traceback:
|
||||
message += "<br>" + "Traceback: <br>" + traceback
|
||||
frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
|
||||
|
||||
frappe.db.set_value(
|
||||
doc.doctype,
|
||||
doc.name,
|
||||
{
|
||||
"error_log": message,
|
||||
"status": status,
|
||||
},
|
||||
)
|
||||
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
@@ -247,7 +261,6 @@ def repost(doc):
|
||||
|
||||
if outgoing_email_account and not isinstance(e, RecoverableErrors):
|
||||
notify_error_to_stock_managers(doc, message)
|
||||
doc.set_status("Failed")
|
||||
finally:
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -327,12 +327,26 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
|
||||
expense_account = None
|
||||
|
||||
if args.get("doctype") == "Purchase Invoice" and item.is_fixed_asset:
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
if item.is_fixed_asset:
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
|
||||
|
||||
expense_account = get_asset_category_account(
|
||||
fieldname="fixed_asset_account", item=args.item_code, company=args.company
|
||||
)
|
||||
if is_cwip_accounting_enabled(item.asset_category):
|
||||
expense_account = get_asset_account(
|
||||
"capital_work_in_progress_account",
|
||||
asset_category=item.asset_category,
|
||||
company=args.company,
|
||||
)
|
||||
elif args.get("doctype") in (
|
||||
"Purchase Invoice",
|
||||
"Purchase Receipt",
|
||||
"Purchase Order",
|
||||
"Material Request",
|
||||
):
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
|
||||
expense_account = get_asset_category_account(
|
||||
fieldname="fixed_asset_account", item=args.item_code, company=args.company
|
||||
)
|
||||
|
||||
# Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master
|
||||
if not args.get("uom"):
|
||||
@@ -1283,7 +1297,7 @@ def get_batch_qty(batch_no, warehouse, item_code):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def apply_price_list(args, as_doc=False):
|
||||
def apply_price_list(args, as_doc=False, doc=None):
|
||||
"""Apply pricelist on a document-like dict object and return as
|
||||
{'parent': dict, 'children': list}
|
||||
|
||||
@@ -1322,7 +1336,7 @@ def apply_price_list(args, as_doc=False):
|
||||
for item in item_list:
|
||||
args_copy = frappe._dict(args.copy())
|
||||
args_copy.update(item)
|
||||
item_details = apply_price_list_on_item(args_copy)
|
||||
item_details = apply_price_list_on_item(args_copy, doc=doc)
|
||||
children.append(item_details)
|
||||
|
||||
if as_doc:
|
||||
@@ -1340,10 +1354,10 @@ def apply_price_list(args, as_doc=False):
|
||||
return {"parent": parent, "children": children}
|
||||
|
||||
|
||||
def apply_price_list_on_item(args):
|
||||
def apply_price_list_on_item(args, doc=None):
|
||||
item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1)
|
||||
item_details = get_price_list_rate(args, item_doc)
|
||||
item_details.update(get_pricing_rule_for_item(args))
|
||||
item_details.update(get_pricing_rule_for_item(args, doc=doc))
|
||||
|
||||
return item_details
|
||||
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
|
||||
frappe.query_reports["Product Bundle Balance"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "date",
|
||||
label: __("Date"),
|
||||
|
||||
@@ -224,6 +224,9 @@ def get_stock_ledger_entries(filters, items):
|
||||
.where((sle2.name.isnull()) & (sle.docstatus < 2) & (sle.item_code.isin(items)))
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
query = query.where(sle.company == filters.get("company"))
|
||||
|
||||
if date := filters.get("date"):
|
||||
query = query.where(sle.posting_date <= date)
|
||||
else:
|
||||
@@ -237,7 +240,7 @@ def get_stock_ledger_entries(filters, items):
|
||||
if warehouse_details:
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
query = query.where(
|
||||
ExistsCriterion(
|
||||
sle.warehouse.isin(
|
||||
frappe.qb.from_(wh)
|
||||
.select(wh.name)
|
||||
.where((wh.lft >= warehouse_details.lft) & (wh.rgt <= warehouse_details.rgt))
|
||||
|
||||
@@ -209,7 +209,9 @@ def repost_future_sle(
|
||||
)
|
||||
affected_transactions.update(obj.affected_transactions)
|
||||
|
||||
distinct_item_warehouses[(args[i].get("item_code"), args[i].get("warehouse"))].reposting_status = True
|
||||
key = (args[i].get("item_code"), args[i].get("warehouse"))
|
||||
if distinct_item_warehouses.get(key):
|
||||
distinct_item_warehouses[key].reposting_status = True
|
||||
|
||||
if obj.new_items_found:
|
||||
for _item_wh, data in distinct_item_warehouses.items():
|
||||
|
||||
@@ -240,6 +240,12 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
)
|
||||
|
||||
def validate_available_qty_for_consumption(self):
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||
== "BOM"
|
||||
):
|
||||
return
|
||||
|
||||
for item in self.get("supplied_items"):
|
||||
precision = item.precision("consumed_qty")
|
||||
if (
|
||||
|
||||
@@ -75,6 +75,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost))
|
||||
|
||||
def test_available_qty_for_consumption(self):
|
||||
set_backflush_based_on("BOM")
|
||||
make_stock_entry(item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
@@ -119,7 +120,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
self.assertRaises(frappe.ValidationError, scr.submit)
|
||||
scr.submit()
|
||||
|
||||
def test_subcontracting_gle_fg_item_rate_zero(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
|
||||
@@ -556,6 +557,21 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
# consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6
|
||||
self.assertEqual(scr.supplied_items[0].consumed_qty, 6)
|
||||
|
||||
# Do not transfer materials to the supplier warehouse and check whether system allows to consumed directly from the supplier's warehouse
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
|
||||
# Transfer RM's
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items, warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
# Create Subcontracting Receipt
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.submit()
|
||||
self.assertEqual(scr.docstatus, 1)
|
||||
|
||||
for item in scr.supplied_items:
|
||||
self.assertFalse(item.available_qty_for_consumption)
|
||||
|
||||
def test_supplied_items_cost_after_reposting(self):
|
||||
# Set Backflush Based On as "BOM"
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
@@ -1248,6 +1248,8 @@ Is Default,Ist Standard,
|
||||
Is Existing Asset,Vermögenswert existiert bereits.,
|
||||
Is Frozen,Ist gesperrt,
|
||||
Is Group,Ist Gruppe,
|
||||
Is Group Warehouse,Ist Lagergruppe,
|
||||
Is Rejected Warehouse,Ist Lager für abgelehnte Ware,
|
||||
Issue,Anfrage,
|
||||
Issue Material,Material ausgeben,
|
||||
Issued,Ausgestellt,
|
||||
@@ -1311,6 +1313,7 @@ Items for Raw Material Request,Artikel für Rohstoffanforderung,
|
||||
Job Card,Jobkarte,
|
||||
Job card {0} created,Jobkarte {0} erstellt,
|
||||
Join,Beitreten,
|
||||
Joining,Beitritt,
|
||||
Journal Entries {0} are un-linked,Buchungssätze {0} sind nicht verknüpft,
|
||||
Journal Entry,Buchungssatz,
|
||||
Journal Entry {0} does not have account {1} or already matched against other voucher,Buchungssatz {0} gehört nicht zu Konto {1} oder ist bereits mit einem anderen Beleg abgeglichen,
|
||||
@@ -2033,6 +2036,7 @@ Product Search,Produkt Suche,
|
||||
Production,Produktion,
|
||||
Production Item,Produktions-Artikel,
|
||||
Products,Produkte,
|
||||
Profile,Profil,
|
||||
Profit and Loss,Gewinn und Verlust,
|
||||
Profit for the year,Jahresüberschuss,
|
||||
Program,Programm,
|
||||
@@ -2094,6 +2098,9 @@ Qty To Manufacture,Herzustellende Menge,
|
||||
Qty Total,Gesamtmenge,
|
||||
Qty for {0},Menge für {0},
|
||||
Qualification,Qualifikation,
|
||||
Qualification Status,Qualifikationsstatus,
|
||||
Qualified By,Qualifiziert von,
|
||||
Qualified on,Qualifiziert am,
|
||||
Quality,Qualität,
|
||||
Quality Action,Qualitätsmaßnahme,
|
||||
Quality Goal.,Qualitätsziel.,
|
||||
@@ -3295,6 +3302,7 @@ Warehouse Type,Lagertyp,
|
||||
'Date' is required,'Datum' ist erforderlich,
|
||||
Budgets,Budgets,
|
||||
Bundle Qty,Bundle Menge,
|
||||
Company Details,Unternehmensdetails,
|
||||
Company GSTIN,Unternehmen GSTIN,
|
||||
Company field is required,Firmenfeld ist erforderlich,
|
||||
Creating Dimensions...,Dimensionen erstellen ...,
|
||||
@@ -3659,6 +3667,7 @@ Performance,Performance,
|
||||
Period based On,Zeitraum basierend auf,
|
||||
Perpetual inventory required for the company {0} to view this report.,"Permanente Bestandsaufnahme erforderlich, damit das Unternehmen {0} diesen Bericht anzeigen kann.",
|
||||
Phone,Telefon,
|
||||
Phone Ext.,Telefon Ext.,
|
||||
Pick List,Auswahlliste,
|
||||
Plaid authentication error,Plaid-Authentifizierungsfehler,
|
||||
Plaid public token error,Plaid public token error,
|
||||
@@ -5096,7 +5105,7 @@ Number of Depreciations Booked,Anzahl der gebuchten Abschreibungen,
|
||||
Finance Books,Finanzbücher,
|
||||
Straight Line,Gerade Linie,
|
||||
Double Declining Balance,Doppelte degressive,
|
||||
Manual,Handbuch,
|
||||
Manual,Manuell,
|
||||
Value After Depreciation,Wert nach Abschreibung,
|
||||
Total Number of Depreciations,Gesamtzahl der Abschreibungen,
|
||||
Frequency of Depreciation (Months),Die Häufigkeit der Abschreibungen (Monate),
|
||||
@@ -5156,6 +5165,7 @@ Maintenance Team Name,Name des Wartungsteams,
|
||||
Maintenance Team Members,Mitglieder des Wartungsteams,
|
||||
Purpose,Zweck,
|
||||
Stock Manager,Lagerleiter,
|
||||
Stock Movement,Lagerbewegung,
|
||||
Asset Movement Item,Vermögensbewegungsgegenstand,
|
||||
Source Location,Quellspeicherort,
|
||||
From Employee,Von Mitarbeiter,
|
||||
@@ -5252,6 +5262,7 @@ Default Bank Account,Standardbankkonto,
|
||||
Is Transporter,Ist Transporter,
|
||||
Represents Company,Repräsentiert das Unternehmen,
|
||||
Supplier Type,Lieferantentyp,
|
||||
Allow Purchase,Einkauf zulassen,
|
||||
Allow Purchase Invoice Creation Without Purchase Order,Erstellen von Eingangsrechnung ohne Bestellung zulassen,
|
||||
Allow Purchase Invoice Creation Without Purchase Receipt,Erstellen von Eingangsrechnung ohne Kaufbeleg ohne Kaufbeleg zulassen,
|
||||
Warn RFQs,Warnung Ausschreibungen,
|
||||
@@ -6028,6 +6039,7 @@ Occupational Hazards and Environmental Factors,Berufsrisiken und Umweltfaktoren,
|
||||
Other Risk Factors,Andere Risikofaktoren,
|
||||
Patient Details,Patientendetails,
|
||||
Additional information regarding the patient,Zusätzliche Informationen zum Patienten,
|
||||
Additional Info,Zusätzliche Informationen,
|
||||
HLC-APP-.YYYY.-,HLC-APP-.YYYY.-,
|
||||
Patient Age,Patient Alter,
|
||||
Get Prescribed Clinical Procedures,Holen Sie sich vorgeschriebene klinische Verfahren,
|
||||
@@ -6194,6 +6206,7 @@ Date Of Retirement,Zeitpunkt der Pensionierung,
|
||||
Department and Grade,Abteilung und Klasse,
|
||||
Reports to,Vorgesetzter,
|
||||
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
|
||||
Attendance & Leaves,Anwesenheit & Urlaub,
|
||||
Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
|
||||
Applicable Holiday List,Geltende Feiertagsliste,
|
||||
Default Shift,Standardverschiebung,
|
||||
@@ -6746,7 +6759,7 @@ Default Costing Rate,Standardkosten,
|
||||
Default Billing Rate,Standard-Rechnungspreis,
|
||||
Dependent Task,Abhängiger Vorgang,
|
||||
Project Type,Projekttyp,
|
||||
% Complete Method,% abgeschlossene Methode,
|
||||
% Complete Method,Fertigstellung bemessen nach,
|
||||
Task Completion,Aufgabenerledigung,
|
||||
Task Progress,Vorgangsentwicklung,
|
||||
% Completed,% abgeschlossen,
|
||||
@@ -6909,6 +6922,7 @@ Restaurant Reservation,Restaurant Reservierung,
|
||||
Waitlisted,Auf der Warteliste,
|
||||
No Show,Nicht angetreten,
|
||||
No of People,Anzahl von Personen,
|
||||
No of Employees,Anzahl der Mitarbeiter,
|
||||
Reservation Time,Reservierungszeit,
|
||||
Reservation End Time,Reservierungsendzeit,
|
||||
No of Seats,Anzahl der Sitze,
|
||||
@@ -6920,6 +6934,7 @@ Default Company Bank Account,Standard-Bankkonto des Unternehmens,
|
||||
From Lead,Aus Lead,
|
||||
Account Manager,Kundenberater,
|
||||
Accounts Manager,Buchhalter,
|
||||
Allow Sales,Verkauf zulassen,
|
||||
Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Auftrag,
|
||||
Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Lieferschein,
|
||||
Default Price List,Standardpreisliste,
|
||||
@@ -6979,7 +6994,8 @@ Not Delivered,Nicht geliefert,
|
||||
Fully Delivered,Komplett geliefert,
|
||||
Partly Delivered,Teilweise geliefert,
|
||||
Not Applicable,Nicht andwendbar,
|
||||
% Delivered,% geliefert,
|
||||
% Delivered,% Geliefert,
|
||||
% Picked,% Kommissioniert,
|
||||
% of materials delivered against this Sales Order,% der für diesen Auftrag gelieferten Materialien,
|
||||
% of materials billed against this Sales Order,% der Materialien welche zu diesem Auftrag gebucht wurden,
|
||||
Not Billed,Nicht abgerechnet,
|
||||
@@ -7113,6 +7129,7 @@ New Income,Neuer Verdienst,
|
||||
New Expenses,Neue Ausgaben,
|
||||
Annual Income,Jährliches Einkommen,
|
||||
Annual Expenses,Jährliche Kosten,
|
||||
Annual Revenue,Jährlicher Umsatz,
|
||||
Bank Balance,Kontostand,
|
||||
Bank Credit Balance,Bankguthaben,
|
||||
Receivables,Forderungen,
|
||||
@@ -7595,6 +7612,7 @@ Actual Qty After Transaction,Tatsächliche Anzahl nach Transaktionen,
|
||||
Stock Value Difference,Lagerwert-Differenz,
|
||||
Stock Queue (FIFO),Lagerverfahren (FIFO),
|
||||
Is Cancelled,Ist storniert,
|
||||
Is Cash or Non Trade Discount,Ist Bar- oder Nicht-Handelsrabatt,
|
||||
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.",
|
||||
Reconciliation JSON,Abgleich JSON (JavaScript Object Notation),
|
||||
@@ -7696,6 +7714,7 @@ Warranty / AMC Status,Status der Garantie / des jährlichen Wartungsvertrags,
|
||||
Resolved By,Entschieden von,
|
||||
Service Address,Serviceadresse,
|
||||
If different than customer address,Falls abweichend von Kundenadresse,
|
||||
"If yes, then this warehouse will be used to store rejected materials","Falls aktiviert, wird dieses Lager verwendet, um abgelehnte Ware zu lagern",
|
||||
Raised By,Gemeldet durch,
|
||||
From Company,Von Unternehmen,
|
||||
Rename Tool,Werkzeug zum Umbenennen,
|
||||
@@ -8940,6 +8959,7 @@ Print Receipt,Druckeingang,
|
||||
Edit Receipt,Beleg bearbeiten,
|
||||
Focus on search input,Konzentrieren Sie sich auf die Sucheingabe,
|
||||
Focus on Item Group filter,Fokus auf Artikelgruppenfilter,
|
||||
Footer will display correctly only in PDF,Die Fußzeile wird nur im PDF korrekt angezeigt,
|
||||
Checkout Order / Submit Order / New Order,Kaufabwicklung / Bestellung abschicken / Neue Bestellung,
|
||||
Add Order Discount,Bestellrabatt hinzufügen,
|
||||
Item Code: {0} is not available under warehouse {1}.,Artikelcode: {0} ist unter Lager {1} nicht verfügbar.,
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user