Compare commits

...

75 Commits

Author SHA1 Message Date
Nabin Hait
b7219dc698 Merge branch 'develop' 2015-07-02 17:58:40 +05:30
Nabin Hait
37c6ce7b1e bumped to version 5.1.2 2015-07-02 18:28:40 +06:00
Nabin Hait
7028ba507e Merge pull request #3561 from nabinhait/develop
Fixes
2015-07-02 17:55:18 +05:30
Nabin Hait
fcee17fd16 Removed purchase item validation 2015-07-02 17:51:35 +05:30
Nabin Hait
5906f02e3a update item image in invoice if item exists 2015-07-02 17:51:35 +05:30
Nabin Hait
721dcb1870 Merge branch 'develop' 2015-07-02 16:33:52 +05:30
Nabin Hait
6385967ccd bumped to version 5.1.1 2015-07-02 17:03:52 +06:00
Nabin Hait
81a977cacf Merge pull request #3559 from nabinhait/develop
Fixes
2015-07-02 16:32:34 +05:30
Nabin Hait
34058ded0e [fix] Update item image in sales invoice 2015-07-02 16:31:55 +05:30
Nabin Hait
e98120b716 [fix] Change log path 2015-07-02 16:29:28 +05:30
Nabin Hait
50e363b24e Merge branch 'develop' 2015-07-02 15:28:49 +05:30
Nabin Hait
bc176b6bf8 bumped to version 5.1.0 2015-07-02 15:58:49 +06:00
Nabin Hait
784b49e54e change log 2015-07-02 15:28:15 +05:30
Nabin Hait
04fb6e1a45 Merge pull request #3558 from nabinhait/develop
Change log
2015-07-02 15:22:20 +05:30
Nabin Hait
dffd7c3889 Change log 2015-07-02 15:21:25 +05:30
Nabin Hait
0c7594e5ed Merge pull request #3557 from rmehta/item-description-in-form
[fix] show item description in form grid based on in_list_view
2015-07-02 15:19:24 +05:30
Rushabh Mehta
5105f909d6 [fix] show item description in form grid based on in_list_view 2015-07-02 15:09:18 +05:30
Nabin Hait
2ab9d6e92c Merge pull request #3552 from nabinhait/develop
Recurring docs should not consider Stopped documents and should be scheduled for hourly
2015-07-02 14:42:57 +05:30
Nabin Hait
7580723ab3 Recurring docs should not consider Stopped documents and should be scheduled for hourly 2015-07-02 14:41:27 +05:30
Nabin Hait
52fef71c14 Merge pull request #3543 from rmehta/against-account-fix
Fix against account to show party instead of account
2015-07-02 14:28:11 +05:30
Rushabh Mehta
2cd02af80a [fix] patch date 2015-07-02 14:25:51 +05:30
Rushabh Mehta
0b0ec3536c [change-log] added 2015-07-02 14:25:51 +05:30
Rushabh Mehta
6b35ea873b [fix] against account in general ledger will show party 2015-07-02 14:25:51 +05:30
Nabin Hait
ecf220a721 Merge pull request #3549 from neilLasrado/item-varients
Validation changed for Item Template cannot have Stock
2015-07-02 14:18:31 +05:30
Nabin Hait
04fb1c7fe4 Merge pull request #3550 from neilLasrado/item-desc
Image feild added to Sales Invoice & Purchase Invoice
2015-07-02 14:17:52 +05:30
Neil Trini Lasrado
53bd62eafc Validation changed for Item Template cannot have Stock 2015-07-02 14:15:23 +05:30
Nabin Hait
ff68bf2609 Merge pull request #3551 from nabinhait/develop
Fixes
2015-07-02 14:10:58 +05:30
Nabin Hait
fdd0db3459 Update project completion percentage and costing after syncing task 2015-07-02 14:09:49 +05:30
Nabin Hait
f92465981c datetime issue fixed in maintenance schedule 2015-07-02 14:09:49 +05:30
Nabin Hait
885c70984d [fix] Always show Sales invoice in Gross profit report 2015-07-02 14:09:49 +05:30
Neil Trini Lasrado
9c15ef903d Image feild added to Sales Invoice & Purchase Invoice 2015-07-02 13:09:57 +05:30
Rushabh Mehta
bc799885d0 [fix] bom client script 2015-07-02 12:44:16 +05:30
Nabin Hait
cb129264cb Merge pull request #3386 from neilLasrado/item-varients
[redesign] Manage Item varients
2015-07-01 18:29:04 +05:30
Neil Trini Lasrado
fe9cd1d875 More Fixes in Manage Varients 2015-07-01 14:41:30 +05:30
Neil Trini Lasrado
64fbe955c7 Modified Date changed for Item Doctype 2015-07-01 14:04:00 +05:30
Neil Trini Lasrado
0988440fd9 Fixes for Manage Variants 2015-07-01 13:17:15 +05:30
Neil Trini Lasrado
274dd4ada0 Fixed Manage Variants Attribute autocomplete appearance. 2015-07-01 12:51:40 +05:30
Neil Trini Lasrado
724fd82419 Fixes in Manage Variants 2015-07-01 12:51:40 +05:30
Neil Trini Lasrado
3f2604eff6 Patch for Item Variants 2015-07-01 12:51:40 +05:30
Neil Trini Lasrado
c761fefb78 more fixes in test records 2015-07-01 12:51:11 +05:30
Neil Trini Lasrado
203cb10ef7 fixes in test cases 2015-07-01 12:51:11 +05:30
Neil Trini Lasrado
90c66b1998 test cases added 2015-07-01 12:51:11 +05:30
Neil Trini Lasrado
fed431f908 Code Fixes in Manage Variants 2015-07-01 12:51:11 +05:30
Neil Trini Lasrado
bd9745ba72 Rename Item Variant from Manage Variants feature Added. 2015-07-01 12:51:11 +05:30
Neil Trini Lasrado
8fb123b20e item variants, creation, deleation and update logic added.
logic added to copy changes in template to variants
2015-07-01 12:51:11 +05:30
Neil Trini Lasrado
333ccd212b variants combination generation logic added 2015-07-01 12:51:10 +05:30
Neil Trini Lasrado
c8cc8b7115 manage variants new doctype created 2015-07-01 12:51:10 +05:30
Nabin Hait
ec44fa95ce Merge pull request #3537 from neilLasrado/project
Fixed Issues in Project Task
2015-06-30 17:29:19 +05:30
Neil Trini Lasrado
a25e8ea0bc Fixed Issues in Project Task 2015-06-30 17:15:13 +05:30
Rushabh Mehta
ea02b9a5c3 Merge pull request #3530 from anandpdoshi/anand-june-29
[fix] Item Variant Attribute autocomplete appearance. Fixes #3488, #3515, #3525
2015-06-30 13:05:19 +05:30
Rushabh Mehta
74a63bf003 Merge pull request #3531 from pdvyas/exchange-rate-api
Change currency exchange rate api to fixer.io
2015-06-30 12:46:52 +05:30
Pratik Vyas
a0f2510b01 Change currency exchange rate api to fixer.io 2015-06-30 12:36:17 +05:30
Anand Doshi
0820161157 [fix] Item Variant Attribute autocomplete appearance. Fixes #3488, #3515, #3525 2015-06-29 20:57:06 -07:00
Nabin Hait
56bc215855 Merge pull request #3505 from neilLasrado/print-format
Recurring Invoice Print Format
2015-06-29 18:54:24 +05:30
Nabin Hait
8687d25f7e Merge pull request #3526 from neilLasrado/minor-fixes
Minor fixes
2015-06-29 18:53:10 +05:30
Nabin Hait
9cea01fa8b Merge pull request #3527 from nabinhait/develop
Journal Entry list view and delete events via query
2015-06-29 18:39:43 +05:30
Nabin Hait
e06d01e3ed Delete events via query instead of delete_doc function, to save time 2015-06-29 18:38:38 +05:30
Nabin Hait
11243a4fb4 Show amount in Journal Entry list view 2015-06-29 18:38:38 +05:30
Neil Trini Lasrado
b9e5cd0df4 Fixed lead status not updating on Creation of oppurtunity issue 2015-06-29 17:01:32 +05:30
Neil Trini Lasrado
a1f1edc786 Typo fixes in Sales Person 2015-06-29 16:04:12 +05:30
Neil Trini Lasrado
e2d8dd0663 added recurring print format to sales/purchase invoice and order 2015-06-29 12:50:41 +05:30
Nabin Hait
d1605c5cb2 Merge branch 'develop' 2015-06-27 13:09:05 +05:30
Nabin Hait
64ca52fb77 bumped to version 5.0.29 2015-06-27 13:39:05 +06:00
Nabin Hait
2f11a3bdaf Merge pull request #3523 from nabinhait/develop
Multiple fixes
2015-06-27 13:07:44 +05:30
Nabin Hait
943dc1f59c Merge branch 'develop' 2015-06-27 13:07:01 +05:30
Nabin Hait
6d1c994bc9 bumped to version 5.0.28 2015-06-27 13:37:01 +06:00
Nabin Hait
93cdee4503 [fix] Escape values in queries 2015-06-27 12:51:00 +05:30
Nabin Hait
dfac6848cc In list view property added in BOM 2015-06-26 14:42:51 +05:30
Nabin Hait
8ad0b4e0b9 Show Issue id in list view from customer login 2015-06-26 14:42:51 +05:30
Nabin Hait
b8cd92f1aa Merge pull request #3517 from neilLasrado/batch
Added validation to prevent transfer of raw material from an expired …
2015-06-25 18:20:46 +05:30
Nabin Hait
1fe48cb820 Merge pull request #3514 from anandpdoshi/anand-june-24
[fix] Use per_billed instead of per_delivered and per_received for open notification
2015-06-25 18:04:26 +05:30
Anand Doshi
723b046c5b [fix] Use per_billed instead of per_delivered and per_received for open notification 2015-06-24 19:21:52 -04:00
Neil Trini Lasrado
035160c9a6 Added validation to prevent transfer of raw material from an expired batch for manufacturing 2015-06-24 15:25:57 +05:30
Nabin Hait
01c7ce1da3 Merge pull request #3508 from nabinhait/develop
[fix] validate items in stock entry for subcontracting against PO
2015-06-23 11:12:23 +05:30
Nabin Hait
9cf2910ba1 [fix] validate items in stock entry for subcontracting against purchase order 2015-06-23 11:07:34 +05:30
66 changed files with 1031 additions and 383 deletions

View File

@@ -1,2 +1,2 @@
from __future__ import unicode_literals
__version__ = '5.0.27'
__version__ = '5.1.2'

View File

@@ -127,7 +127,7 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga
against_voucher_amount = flt(frappe.db.sql("""
select sum(ifnull(debit, 0)) - sum(ifnull(credit, 0))
from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s
and account = %s and ifnull(party_type, '')=%s and ifnull(party, '')=%s
and account = %s and ifnull(party_type, '')=%s and ifnull(party, '')=%s
and ifnull(against_voucher, '') = ''""",
(against_voucher, account, cstr(party_type), cstr(party)))[0][0])
@@ -158,3 +158,22 @@ def validate_frozen_account(account, adv_adj=None):
frappe.throw(_("Account {0} is frozen").format(account))
elif frozen_accounts_modifier not in frappe.get_roles():
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
def update_against_account(voucher_type, voucher_no):
entries = frappe.db.get_all("GL Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["name", "party", "against", "debit", "credit", "account"])
accounts_debited, accounts_credited = [], []
for d in entries:
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
for d in entries:
if flt(d.debit > 0):
new_against = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0):
new_against = ", ".join(list(set(accounts_debited)))
if d.against != new_against:
frappe.db.set_value("GL Entry", d.name, "against", new_against)

View File

@@ -53,7 +53,7 @@
"fieldname": "posting_date",
"fieldtype": "Date",
"in_filter": 1,
"in_list_view": 1,
"in_list_view": 0,
"label": "Posting Date",
"no_copy": 1,
"oldfieldname": "posting_date",
@@ -445,7 +445,7 @@
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
"modified": "2015-04-27 20:32:31.655580",
"modified": "2015-06-29 15:28:12.529019",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -81,7 +81,7 @@ class JournalEntry(AccountsController):
frappe.throw(_("Row {0}: Party Type and Party is only applicable against Receivable / Payable account").format(d.idx))
def check_credit_limit(self):
customers = list(set([d.party for d in self.get("accounts")
customers = list(set([d.party for d in self.get("accounts")
if d.party_type=="Customer" and d.party and flt(d.debit) > 0]))
if customers:
from erpnext.selling.doctype.customer.customer import check_credit_limit
@@ -243,8 +243,8 @@ class JournalEntry(AccountsController):
def set_against_account(self):
accounts_debited, accounts_credited = [], []
for d in self.get("accounts"):
if flt(d.debit > 0): accounts_debited.append(d.account)
if flt(d.credit) > 0: accounts_credited.append(d.account)
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
for d in self.get("accounts"):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
@@ -274,9 +274,9 @@ class JournalEntry(AccountsController):
r.append(_('Reference #{0} dated {1}').format(self.cheque_no, formatdate(self.cheque_date)))
else:
msgprint(_("Please enter Reference date"), raise_exception=frappe.MandatoryError)
company_currency = get_company_currency(self.company)
for d in self.get('accounts'):
if d.against_invoice and d.credit:
r.append(_("{0} against Sales Invoice {1}").format(fmt_money(flt(d.credit), currency = company_currency), \
@@ -426,7 +426,7 @@ class JournalEntry(AccountsController):
def validate_expense_claim(self):
for d in self.accounts:
if d.against_expense_claim:
sanctioned_amount, reimbursed_amount = frappe.db.get_value("Expense Claim",
sanctioned_amount, reimbursed_amount = frappe.db.get_value("Expense Claim",
d.against_expense_claim, ("total_sanctioned_amount", "total_amount_reimbursed"))
pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount)
if d.debit > pending_amount:

View File

@@ -224,3 +224,4 @@ cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
else
cur_frm.pformat.print_heading = __("Purchase Invoice");
}

View File

@@ -926,12 +926,21 @@
"no_copy": 1,
"permlevel": 0,
"print_hide": 1
},
{
"depends_on": "eval:doc.is_recurring==1",
"fieldname": "recurring_print_format",
"fieldtype": "Link",
"label": "Recurring Print Format",
"options": "Print Format",
"permlevel": 0,
"precision": ""
}
],
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
"modified": "2015-06-16 16:46:47.308287",
"modified": "2015-06-22 07:30:06.743438",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -164,7 +164,7 @@ class PurchaseInvoice(BuyingController):
elif item.expense_account not in against_accounts:
# if no auto_accounting_for_stock or not a stock item
against_accounts.append(item.expense_account)
self.against_expense_account = ",".join(against_accounts)
def po_required(self):
@@ -271,7 +271,7 @@ class PurchaseInvoice(BuyingController):
gl_entries.append(
self.get_gl_dict({
"account": tax.account_head,
"against": self.credit_to,
"against": self.supplier,
"debit": tax.add_deduct_tax == "Add" and tax.base_tax_amount_after_discount_amount or 0,
"credit": tax.add_deduct_tax == "Deduct" and tax.base_tax_amount_after_discount_amount or 0,
"remarks": self.remarks,
@@ -295,7 +295,7 @@ class PurchaseInvoice(BuyingController):
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"against": self.credit_to,
"against": self.supplier,
"debit": item.base_net_amount,
"remarks": self.remarks,
"cost_center": item.cost_center
@@ -315,7 +315,7 @@ class PurchaseInvoice(BuyingController):
gl_entries.append(
self.get_gl_dict({
"account": stock_received_but_not_billed,
"against": self.credit_to,
"against": self.supplier,
"debit": flt(item.item_tax_amount, self.precision("item_tax_amount", item)),
"remarks": self.remarks or "Accounting Entry for Stock"
})
@@ -341,7 +341,7 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict({
"account": expenses_included_in_valuation,
"cost_center": cost_center,
"against": self.credit_to,
"against": self.supplier,
"credit": applicable_amount,
"remarks": self.remarks or "Accounting Entry for Stock"
})
@@ -355,7 +355,7 @@ class PurchaseInvoice(BuyingController):
gl_entries.append(
self.get_gl_dict({
"account": self.write_off_account,
"against": self.credit_to,
"against": self.supplier,
"credit": flt(self.write_off_amount),
"remarks": self.remarks,
"cost_center": self.write_off_cost_center
@@ -374,7 +374,7 @@ class PurchaseInvoice(BuyingController):
self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
self.make_gl_entries_on_cancel()
self.update_project()
def update_project(self):
project_list = []
for d in self.items:
@@ -384,7 +384,7 @@ class PurchaseInvoice(BuyingController):
project.update_purchase_costing()
project.save()
project_list.append(d.project_name)
def validate_supplier_invoice(self):
if self.bill_date:
if getdate(self.bill_date) > getdate(self.posting_date):
@@ -410,4 +410,4 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
and tabAccount.company = '%(company)s'
and tabAccount.%(key)s LIKE '%(txt)s'
%(mcond)s""" % {'company': filters['company'], 'key': searchfield,
'txt': "%%%s%%" % txt, 'mcond':get_match_cond(doctype)})
'txt': "%%%s%%" % frappe.db.escape(txt), 'mcond':get_match_cond(doctype)})

View File

@@ -49,6 +49,23 @@
"read_only": 0,
"width": "300px"
},
{
"fieldname": "image",
"fieldtype": "Attach",
"hidden": 1,
"label": "Image",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "image_view",
"fieldtype": "Image",
"label": "Image View",
"options": "image",
"permlevel": 0,
"precision": "",
"print_hide": 1
},
{
"fieldname": "quantity_and_rate",
"fieldtype": "Section Break",
@@ -452,7 +469,7 @@
],
"idx": 1,
"istable": 1,
"modified": "2015-06-02 14:18:56.294949",
"modified": "2015-07-02 03:00:44.496683",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -36,7 +36,7 @@ def get_items(price_list, sales_or_purchase, item=None):
if(locate(%(_name)s, i.item_name), locate(%(_name)s, i.item_name), 99999),
if(locate(%(_name)s, i.variant_of), locate(%(_name)s, i.variant_of), 99999),
if(locate(%(_name)s, i.item_group), locate(%(_name)s, i.item_group), 99999),"""
args["name"] = "%%%s%%" % item
args["name"] = "%%%s%%" % frappe.db.escape(item)
args["_name"] = item.replace("%", "")
# locate function is used to sort by closest match from the beginning of the value

View File

@@ -392,8 +392,6 @@ cur_frm.cscript.on_submit = function(doc, cdt, cdn) {
}
}
cur_frm.set_query("debit_to", function(doc) {
return{
filters: [

View File

@@ -1227,6 +1227,15 @@
"print_hide": 1,
"read_only": 0
},
{
"depends_on": "eval:doc.is_recurring==1",
"fieldname": "recurring_print_format",
"fieldtype": "Link",
"label": "Recurring Print Format",
"options": "Print Format",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "against_income_account",
"fieldtype": "Small Text",
@@ -1244,7 +1253,7 @@
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
"modified": "2015-06-16 16:45:06.618286",
"modified": "2015-06-22 06:39:22.072544",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -503,7 +503,7 @@ class SalesInvoice(SellingController):
gl_entries.append(
self.get_gl_dict({
"account": tax.account_head,
"against": self.debit_to,
"against": self.customer,
"credit": flt(tax.base_tax_amount_after_discount_amount),
"remarks": self.remarks,
"cost_center": tax.cost_center
@@ -517,7 +517,7 @@ class SalesInvoice(SellingController):
gl_entries.append(
self.get_gl_dict({
"account": item.income_account,
"against": self.debit_to,
"against": self.customer,
"credit": item.base_net_amount,
"remarks": self.remarks,
"cost_center": item.cost_center
@@ -548,7 +548,7 @@ class SalesInvoice(SellingController):
gl_entries.append(
self.get_gl_dict({
"account": self.cash_bank_account,
"against": self.debit_to,
"against": self.customer,
"debit": self.paid_amount,
"remarks": self.remarks,
})
@@ -572,7 +572,7 @@ class SalesInvoice(SellingController):
gl_entries.append(
self.get_gl_dict({
"account": self.write_off_account,
"against": self.debit_to,
"against": self.customer,
"debit": self.write_off_amount,
"remarks": self.remarks,
"cost_center": self.write_off_cost_center
@@ -587,7 +587,7 @@ def get_list_context(context=None):
@frappe.whitelist()
def get_bank_cash_account(mode_of_payment, company):
account = frappe.db.get_value("Mode of Payment Account",
account = frappe.db.get_value("Mode of Payment Account",
{"parent": mode_of_payment, "company": company}, "default_account")
if not account:
frappe.msgprint(_("Please set default Cash or Bank account in Mode of Payment {0}").format(mode_of_payment))
@@ -611,7 +611,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
and tabAccount.company = '%(company)s'
and tabAccount.%(key)s LIKE '%(txt)s'
%(mcond)s""" % {'company': filters['company'], 'key': searchfield,
'txt': "%%%s%%" % txt, 'mcond':get_match_cond(doctype)})
'txt': "%%%s%%" % frappe.db.escape(txt), 'mcond':get_match_cond(doctype)})
@frappe.whitelist()
def make_delivery_note(source_name, target_doc=None):

View File

@@ -68,6 +68,23 @@
"reqd": 1,
"width": "200px"
},
{
"fieldname": "image",
"fieldtype": "Attach",
"hidden": 1,
"label": "Image",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "image_view",
"fieldtype": "Image",
"label": "Image View",
"options": "image",
"permlevel": 0,
"precision": "",
"print_hide": 1
},
{
"fieldname": "quantity_and_rate",
"fieldtype": "Section Break",
@@ -505,7 +522,7 @@
],
"idx": 1,
"istable": 1,
"modified": "2015-06-02 14:18:45.176726",
"modified": "2015-07-02 02:59:08.413213",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -175,16 +175,15 @@ class GrossProfitGenerator(object):
else:
if row.update_stock or row.dn_detail:
parenttype, parent, item_row = row.parenttype, row.parent, row.item_row
if row.dn_detail:
row.parenttype = "Delivery Note"
row.parent = row.delivery_note
row.item_row = row.dn_detail
parenttype, parent, item_row = "Delivery Note", row.delivery_note, row.dn_detail
my_sle = self.sle.get((item_code, row.warehouse))
for i, sle in enumerate(my_sle):
# find the stock valution rate from stock ledger entry
if sle.voucher_type == row.parenttype and row.parent == sle.voucher_no and \
sle.voucher_detail_no == row.item_row:
if sle.voucher_type == parenttype and parent == sle.voucher_no and \
sle.voucher_detail_no == item_row:
previous_stock_value = len(my_sle) > i+1 and \
flt(my_sle[i+1].stock_value) or 0.0
return previous_stock_value - flt(sle.stock_value)

View File

@@ -868,12 +868,21 @@
"no_copy": 1,
"permlevel": 0,
"print_hide": 1
},
{
"depends_on": "eval:doc.is_recurring==1",
"fieldname": "recurring_print_format",
"fieldtype": "Link",
"label": "Recurring Print Format",
"options": "Print Format",
"permlevel": 0,
"precision": ""
}
],
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
"modified": "2015-06-15 15:38:56.794601",
"modified": "2015-06-22 07:30:36.259753",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -0,0 +1 @@
- Open notification of Sales Order and Purchase Order based on whether Invoice is created against them. For eg. If a Sales Order is not Invoiced, it will be considered as open. Previously it was considered open if Delivery Note was created against Sales Order.

View File

@@ -0,0 +1,8 @@
- Item variants is now manageable via dedicated tool **Manage Variants**. To learn about it, check [Manual Page for Item variants](https://manual.erpnext.com/contents/stock/item/item-variants)
- Against account in General Ledger will show Party instead of Account (which is not useful)
- Print format for recurring documents can be set by the users
- Recurring documents won't be created for Stopped Sales / Purchase Orders.
- Lead status will be changed to 'Opportunity' when Lead converted to Opportunity
- Amount in Journal Entry list view
- Currency exchange rate is now automatically fetched from fixer.io, instead of jsonrates.com
- Item image is now available in Sales / Purchase Invoice

View File

@@ -194,7 +194,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
and tabBOM.is_active=1
and tabBOM.%(key)s like "%(txt)s"
%(fcond)s %(mcond)s
limit %(start)s, %(page_len)s """ % {'key': searchfield, 'txt': "%%%s%%" % txt,
limit %(start)s, %(page_len)s """ % {'key': searchfield, 'txt': "%%%s%%" % frappe.db.escape(txt),
'fcond': get_filters_cond(doctype, filters, conditions),
'mcond':get_match_cond(doctype), 'start': start, 'page_len': page_len})
@@ -207,7 +207,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
where `tabProject`.status not in ("Completed", "Cancelled")
and %(cond)s `tabProject`.name like "%(txt)s" %(mcond)s
order by `tabProject`.name asc
limit %(start)s, %(page_len)s """ % {'cond': cond,'txt': "%%%s%%" % txt,
limit %(start)s, %(page_len)s """ % {'cond': cond,'txt': "%%%s%%" % frappe.db.escape(txt),
'mcond':get_match_cond(doctype),'start': start, 'page_len': page_len})
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters):

View File

@@ -33,11 +33,13 @@ def manage_recurring_documents(doctype, next_date=None, commit=True):
next_date = next_date or nowdate()
date_field = date_field_map[doctype]
condition = " and ifnull(status, '') != 'Stopped'" if doctype in ("Sales Order", "Purchase Order") else ""
recurring_documents = frappe.db.sql("""select name, recurring_id
from `tab{}` where ifnull(is_recurring, 0)=1
and docstatus=1 and next_date='{}'
and next_date <= ifnull(end_date, '2199-12-31')""".format(doctype, next_date))
from `tab{0}` where ifnull(is_recurring, 0)=1
and docstatus=1 and next_date=%s
and next_date <= ifnull(end_date, '2199-12-31') {1}""".format(doctype, condition), next_date)
exception_list = []
for ref_document, recurring_id in recurring_documents:
@@ -124,7 +126,7 @@ def send_notification(new_rv):
frappe.sendmail(new_rv.notification_email_address,
subject= _("New {0}: #{1}").format(new_rv.doctype, new_rv.name),
message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name),
attachments = [frappe.attach_print(new_rv.doctype, new_rv.name, file_name=new_rv.name)])
attachments = [frappe.attach_print(new_rv.doctype, new_rv.name, file_name=new_rv.name, print_format=new_rv.recurring_print_format)])
def notify_errors(doc, doctype, party, owner):
from frappe.utils.user import get_system_managers

View File

@@ -75,8 +75,7 @@ class Lead(SellingController):
return frappe.db.get_value("Customer", {"lead_name": self.name})
def has_opportunity(self):
return frappe.db.get_value("Opportunity", {"lead": self.name, "docstatus": 1,
"status": ["!=", "Lost"]})
return frappe.db.get_value("Opportunity", {"lead": self.name, "status": ["!=", "Lost"]})
@frappe.whitelist()
def make_customer(source_name, target_doc=None):

View File

@@ -5,7 +5,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd. and Contributors"
app_description = "Open Source Enterprise Resource Planning for Small and Midsized Organizations"
app_icon = "icon-th"
app_color = "#e74c3c"
app_version = "5.0.27"
app_version = "5.1.2"
error_report_email = "support@erpnext.com"
@@ -78,8 +78,10 @@ doc_events = {
}
scheduler_events = {
"hourly": [
"erpnext.controllers.recurring_document.create_recurring_documents"
],
"daily": [
"erpnext.controllers.recurring_document.create_recurring_documents",
"erpnext.stock.reorder_item.reorder_item",
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.support.doctype.issue.issue.auto_close_tickets",

View File

@@ -61,7 +61,7 @@ var get_bom_material_detail= function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.item_code) {
return frappe.call({
doc: cur_frm.doc,
doc: doc,
method: "get_bom_material_detail",
args: {
'item_code': d.item_code,
@@ -234,5 +234,3 @@ frappe.ui.form.on("BOM", "with_operations", function(frm) {
cur_frm.cscript.image = function() {
refresh_field("image_view");
}

View File

@@ -12,7 +12,7 @@
"fieldname": "item",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 0,
"in_list_view": 1,
"label": "Item",
"oldfieldname": "item",
"oldfieldtype": "Link",
@@ -54,7 +54,7 @@
"fieldname": "is_active",
"fieldtype": "Check",
"hidden": 0,
"in_list_view": 0,
"in_list_view": 1,
"label": "Is Active",
"no_copy": 1,
"oldfieldname": "is_active",
@@ -67,7 +67,7 @@
"default": "1",
"fieldname": "is_default",
"fieldtype": "Check",
"in_list_view": 0,
"in_list_view": 1,
"label": "Is Default",
"no_copy": 1,
"oldfieldname": "is_default",
@@ -279,7 +279,7 @@
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"modified": "2015-03-03 14:22:44.725097",
"modified": "2015-06-26 02:02:30.705279",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",

View File

@@ -166,4 +166,8 @@ erpnext.patches.v5_0.portal_fixes
erpnext.patches.v5_0.reset_values_in_tools
execute:frappe.delete_doc("Page", "users")
erpnext.patches.v5_0.update_material_transferred_for_manufacturing_again
erpnext.patches.v5_0.index_on_account_and_gl_entry
erpnext.patches.v5_0.index_on_account_and_gl_entry
execute:frappe.db.sql("""delete from `tabProject Task`""")
erpnext.patches.v5_0.item_variants
erpnext.patches.v5_0.update_item_desc_in_invoice
erpnext.patches.v5_1.fix_against_account

View File

@@ -0,0 +1,16 @@
import frappe
def execute():
frappe.reload_doctype("Item")
for dt in ["manage_variants", "manage_variants_item", "variant_attribute"]:
frappe.reload_doc("stock", "doctype", dt)
for d in frappe.get_list("Item", filters={"has_variants":1}):
manage_variant = frappe.new_doc("Manage Variants")
manage_variant.item_code = d.name
manage_variant.attributes = frappe.db.sql("select item_attribute as attribute, item_attribute_value as attribute_value \
from `tabItem Variant` where parent = %s", d.name, as_dict=1)
if manage_variant.attributes:
manage_variant.generate_combinations()
manage_variant.create_variants()
frappe.delete_doc("DocType", "Item Variant")

View File

@@ -0,0 +1,51 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.website.utils import find_first_image
from frappe.utils import cstr
import re
def execute():
item_details = frappe._dict()
for d in frappe.db.sql("select name, description, image from `tabItem`", as_dict=1):
description = cstr(d.description).strip()
item_details.setdefault(d.name, frappe._dict({
"description": description,
"image": d.image
}))
dt_list= ["Sales Invoice Item","Purchase Invoice Item"]
for dt in dt_list:
frappe.reload_doctype(dt)
records = frappe.db.sql("""select name, item_code, description from `tab{0}`
where ifnull(item_code, '') != '' and description is not null """.format(dt), as_dict=1)
count = 1
for d in records:
if item_details.get(d.item_code) and cstr(d.description) == item_details.get(d.item_code).description:
desc = item_details.get(d.item_code).description
image = item_details.get(d.item_code).image
else:
desc, image = extract_image_and_description(cstr(d.description))
if not image:
item_detail = item_details.get(d.item_code)
if item_detail:
image = item_detail.image
frappe.db.sql("""update `tab{0}` set description = %s, image = %s
where name = %s """.format(dt), (desc, image, d.name))
count += 1
if count % 500 == 0:
frappe.db.commit()
def extract_image_and_description(data):
image_url = find_first_image(data)
desc = data
for tag in ("img", "table", "tr", "td"):
desc = re.sub("\</*{0}[^>]*\>".format(tag), "", desc)
return desc, image_url

View File

View File

@@ -0,0 +1,37 @@
from __future__ import unicode_literals
import frappe
from erpnext.accounts.doctype.gl_entry.gl_entry import update_against_account
def execute():
from_date = "2015-05-01"
for doc in frappe.get_all("Journal Entry",
filters={"creation": (">", from_date), "docstatus": "1"}):
# update in gl_entry
update_against_account("Journal Entry", doc.name)
# update in jv
doc = frappe.get_doc("Journal Entry", doc.name)
doc.set_against_account()
doc.db_update()
for doc in frappe.get_all("Sales Invoice",
filters={"creation": (">", from_date), "docstatus": "1"},
fields=["name", "customer"]):
frappe.db.sql("""update `tabGL Entry` set against=%s
where voucher_type='Sales Invoice' and voucher_no=%s
and credit > 0 and ifnull(party, '')=''""",
(doc.customer, doc.name))
for doc in frappe.get_all("Purchase Invoice",
filters={"creation": (">", from_date), "docstatus": "1"},
fields=["name", "supplier"]):
frappe.db.sql("""update `tabGL Entry` set against=%s
where voucher_type='Purchase Invoice' and voucher_no=%s
and debit > 0 and ifnull(party, '')=''""",
(doc.supplier, doc.name))

View File

@@ -35,6 +35,7 @@ class Project(Document):
def validate(self):
self.validate_dates()
self.sync_tasks()
self.tasks = []
def validate_dates(self):
if self.expected_start_date and self.expected_end_date:
@@ -45,6 +46,8 @@ class Project(Document):
"""sync tasks and remove table"""
if self.flags.dont_sync_tasks: return
task_added_or_deleted = False
task_names = []
for t in self.tasks:
if t.task_id:
@@ -52,6 +55,7 @@ class Project(Document):
else:
task = frappe.new_doc("Task")
task.project = self.name
task_added_or_deleted = True
task.update({
"subject": t.title,
@@ -69,17 +73,22 @@ class Project(Document):
# delete
for t in frappe.get_all("Task", ["name"], {"project": self.name, "name": ("not in", task_names)}):
frappe.delete_doc("Task", t.name)
task_added_or_deleted = True
if task_added_or_deleted:
self.update_project()
self.tasks = []
def update_project(self):
self.update_percent_complete()
self.update_costing()
def update_percent_complete(self):
total = frappe.db.sql("""select count(*) from tabTask where project=%s""",
self.name)[0][0]
total = frappe.db.sql("""select count(*) from tabTask where project=%s""", self.name)[0][0]
if total:
completed = frappe.db.sql("""select count(*) from tabTask where
project=%s and status in ('Closed', 'Cancelled')""", self.name)[0][0]
frappe.db.set_value("Project", self.name, "percent_complete",
int(float(completed) / total * 100))
self.percent_complete = flt(completed) / total * 100
def update_costing(self):
total_cost = frappe.db.sql("""select sum(total_costing_amount) as costing_amount,

View File

@@ -5,3 +5,4 @@ from __future__ import unicode_literals
import frappe
test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"]

View File

@@ -43,15 +43,8 @@ class Task(Document):
def on_update(self):
self.check_recursion()
self.reschedule_dependent_tasks()
self.update_percentage()
self.update_project()
def update_percentage(self):
"""update percent complete in project"""
if self.project and not self.flags.from_project:
project = frappe.get_doc("Project", self.project)
project.run_method("update_percent_complete")
def update_total_expense_claim(self):
self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
where project = %s and task = %s and approval_status = "Approved" and docstatus=1""",(self.project, self.name))
@@ -70,10 +63,10 @@ class Task(Document):
self.act_end_date= tl.end_date
def update_project(self):
if self.project and frappe.db.exists("Project", self.project):
if self.project and not self.flags.from_project:
project = frappe.get_doc("Project", self.project)
project.flags.dont_sync_tasks = True
project.update_costing()
project.update_project()
project.save()
def check_recursion(self):
@@ -141,7 +134,7 @@ def get_project(doctype, txt, searchfield, start, page_len, filters):
%(mcond)s
order by name
limit %(start)s, %(page_len)s """ % {'key': searchfield,
'txt': "%%%s%%" % txt, 'mcond':get_match_cond(doctype),
'txt': "%%%s%%" % frappe.db.escape(txt), 'mcond':get_match_cond(doctype),
'start': start, 'page_len': page_len})

View File

@@ -37,6 +37,16 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(this.frm.fields_dict["items"]) {
this["items_remove"] = this.calculate_taxes_and_totals;
}
if(this.frm.fields_dict["recurring_print_format"]) {
this.frm.set_query("recurring_print_format", function(doc) {
return{
filters: [
['Print Format', 'doc_type', '=', cur_frm.doctype],
]
}
});
}
},
onload_post_render: function() {
@@ -782,3 +792,5 @@ frappe.ui.form.on(cur_frm.doctype, "discount_amount", function(frm) {
cur_frm.cscript.set_dynamic_labels();
cur_frm.cscript.calculate_taxes_and_totals();
})

View File

@@ -183,5 +183,3 @@ cur_frm.cscript.on_submit = function(doc, cdt, cdn) {
cur_frm.email_doc(frappe.boot.notification_settings.sales_order_message);
}
};
;

View File

@@ -1074,13 +1074,22 @@
"no_copy": 1,
"permlevel": 0,
"print_hide": 1
},
{
"depends_on": "eval:doc.is_recurring==1",
"fieldname": "recurring_print_format",
"fieldtype": "Link",
"label": "Recurring Print Format",
"options": "Print Format",
"permlevel": 0,
"precision": ""
}
],
"icon": "icon-file-text",
"idx": 1,
"is_submittable": 1,
"issingle": 0,
"modified": "2015-06-15 15:36:38.898462",
"modified": "2015-06-22 07:29:24.379272",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -58,7 +58,7 @@
"fieldname": "description",
"fieldtype": "Small Text",
"in_filter": 1,
"in_list_view": 1,
"in_list_view": 0,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Small Text",
@@ -498,7 +498,7 @@
],
"idx": 1,
"istable": 1,
"modified": "2015-05-27 02:47:15.134435",
"modified": "2015-07-02 05:37:29.289574",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@@ -122,7 +122,7 @@ erpnext.SalesChart = Class.extend({
if(me.ctype == "Sales Person") {
fields.splice(-1, 0, {fieldtype:'Link', fieldname:'employee', label:__('Employee'),
options:'Employee', description: __("Please enter Employee Id of this sales parson")});
options:'Employee', description: __("Please enter Employee Id of this sales person")});
}
// the dialog

View File

@@ -66,20 +66,6 @@
"label": "Disable Rounded Total",
"permlevel": 0,
"read_only": 0
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"permlevel": 0,
"precision": ""
},
{
"description": "For automatic exchange rates go to jsonrates.com and signup for an API key",
"fieldname": "jsonrates_api_key",
"fieldtype": "Data",
"label": "jsonrates.com API Key",
"permlevel": 0,
"precision": ""
}
],
"hide_toolbar": 0,
@@ -87,7 +73,7 @@
"idx": 1,
"in_create": 1,
"issingle": 1,
"modified": "2015-05-07 05:43:49.760061",
"modified": "2015-06-30 03:00:26.420003",
"modified_by": "Administrator",
"module": "Setup",
"name": "Global Defaults",

View File

@@ -17,7 +17,6 @@ keydict = {
'hide_currency_symbol':'hide_currency_symbol',
'account_url':'account_url',
'disable_rounded_total': 'disable_rounded_total',
'jsonrates_api_key': 'jsonrates_api_key'
}
from frappe.model.document import Document

View File

@@ -60,20 +60,21 @@ def before_tests():
@frappe.whitelist()
def get_exchange_rate(from_currency, to_currency):
jsonrates_api_key = frappe.conf.jsonrates_api_key or frappe.db.get_default("jsonrates_api_key")
if jsonrates_api_key:
try:
cache = frappe.cache()
key = "currency_exchange_rate:{0}:{1}".format(from_currency, to_currency)
value = cache.get(key)
if not value:
import requests
response = requests.get("http://jsonrates.com/get/?from={0}&to={1}&apiKey={2}".format(from_currency,
to_currency, jsonrates_api_key))
response = requests.get("http://api.fixer.io/latest", params={
"base": from_currency,
"symbols": to_currency
})
# expire in 24 hours
value = response.json().get("rate")
response.raise_for_status()
value = response.json()["rates"][to_currency]
cache.setex(key, value, 24 * 60 * 60)
return flt(value)
else:
except:
exchange = "%s-%s" % (from_currency, to_currency)
return flt(frappe.db.get_value("Currency Exchange", exchange, "exchange_rate"))

View File

@@ -14,7 +14,7 @@ def get_notification_config():
"Contact": {"status": "Open"},
"Opportunity": {"status": "Open"},
"Quotation": {"docstatus": 0},
"Sales Order": { "per_delivered": ("<", 100), "status": ("!=", "Stopped"), "docstatus": ("<", 2) },
"Sales Order": { "per_billed": ("<", 100), "status": ("!=", "Stopped"), "docstatus": ("<", 2) },
"Journal Entry": {"docstatus": 0},
"Sales Invoice": { "outstanding_amount": (">", 0), "docstatus": ("<", 2) },
"Purchase Invoice": {"docstatus": 0},
@@ -25,7 +25,7 @@ def get_notification_config():
"Delivery Note": {"docstatus": 0},
"Stock Entry": {"docstatus": 0},
"Material Request": {"docstatus": 0},
"Purchase Order": { "per_received": ("<", 100), "status": ("!=", "Stopped"), "docstatus": ("<", 2) },
"Purchase Order": { "per_billed": ("<", 100), "status": ("!=", "Stopped"), "docstatus": ("<", 2) },
"Production Order": { "status": "In Process" },
"BOM": {"docstatus": 0},
"Timesheet": {"docstatus": 0},

View File

@@ -5,39 +5,6 @@ frappe.provide("erpnext.item");
frappe.ui.form.on("Item", {
onload: function(frm) {
var df = frappe.meta.get_docfield("Item Variant", "item_attribute_value");
df.on_make = function(field) {
field.$input.autocomplete({
minLength: 0,
minChars: 0,
source: function(request, response) {
frappe.call({
method:"frappe.client.get_list",
args:{
doctype:"Item Attribute Value",
filters: [
["parent","=", field.doc.item_attribute],
["attribute_value", "like", request.term + "%"]
],
fields: ["attribute_value"]
},
callback: function(r) {
response($.map(r.message, function(d) { return d.attribute_value; }));
}
});
},
select: function(event, ui) {
field.$input.val(ui.item.value);
field.$input.trigger("change");
},
focus: function( event, ui ) {
if(ui.item.action) {
return false;
}
},
});
}
erpnext.item.setup_queries(frm);
},
@@ -113,8 +80,14 @@ frappe.ui.form.on("Item", {
method: "copy_specification_from_item_group"
});
},
is_stock_item: function(frm) {
erpnext.item.toggle_reqd(frm);
},
manage_variants: function(frm) {
frappe.route_options = {"item_code": frm.doc.name };
frappe.set_route("List", "Manage Variants");
}
});

View File

@@ -12,7 +12,7 @@
{
"fieldname": "name_and_description_section",
"fieldtype": "Section Break",
"label": "Name and Description",
"label": "",
"no_copy": 0,
"oldfieldtype": "Section Break",
"options": "icon-flag",
@@ -167,16 +167,17 @@
"search_index": 0
},
{
"depends_on": "eval:!!!doc.variant_of",
"depends_on": "eval:!doc.variant_of",
"fieldname": "variants_section",
"fieldtype": "Section Break",
"label": "Variants",
"label": "Variant",
"permlevel": 0,
"precision": ""
},
{
"default": "0",
"description": "Automatically set. If this item has variants, then it cannot be selected in sales orders etc.",
"depends_on": "",
"description": "If this item has variants, then it cannot be selected in sales orders etc.",
"fieldname": "has_variants",
"fieldtype": "Check",
"label": "Has Variants",
@@ -186,15 +187,37 @@
"read_only": 0
},
{
"depends_on": "has_variants",
"description": "A new variant (Item) will be created for each attribute value combination",
"fieldname": "variants",
"fieldtype": "Table",
"label": "Variants",
"options": "Item Variant",
"fieldname": "column_break_18",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"depends_on": "has_variants",
"fieldname": "manage_variants",
"fieldtype": "Button",
"label": "Manage Variants",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "section_break_20",
"fieldtype": "Section Break",
"permlevel": 0,
"precision": ""
},
{
"depends_on": "variant_of",
"fieldname": "attributes",
"fieldtype": "Table",
"hidden": 0,
"label": "Attributes",
"no_copy": 1,
"options": "Variant Attribute",
"permlevel": 0,
"precision": "",
"read_only": 0
},
{
"fieldname": "inventory",
"fieldtype": "Section Break",
@@ -707,7 +730,7 @@
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Default BOM",
"no_copy": 0,
"no_copy": 1,
"oldfieldname": "default_bom",
"oldfieldtype": "Link",
"options": "BOM",
@@ -877,9 +900,9 @@
}
],
"icon": "icon-tag",
"idx": 1,
"idx": 1,
"max_attachments": 1,
"modified": "2015-05-22 02:16:57.435105",
"modified": "2015-07-01 17:20:18.204558",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -9,10 +9,9 @@ from frappe.website.website_generator import WebsiteGenerator
from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for, get_parent_item_groups
from frappe.website.render import clear_cache
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
import copy
from erpnext.stock.doctype.manage_variants.manage_variants import update_variant
class WarehouseNotSet(frappe.ValidationError): pass
class DuplicateVariant(frappe.ValidationError): pass
class ItemTemplateCannotHaveStock(frappe.ValidationError): pass
class Item(WebsiteGenerator):
@@ -48,9 +47,6 @@ class Item(WebsiteGenerator):
if self.image and not self.website_image:
self.website_image = self.image
if self.variant_of:
self.copy_attributes_to_variant(frappe.get_doc("Item", self.variant_of), self)
self.check_warehouse_is_set_for_stock_item()
self.check_stock_uom_with_bin()
self.add_default_uom_in_conversion_factor_table()
@@ -63,9 +59,10 @@ class Item(WebsiteGenerator):
self.cant_change()
self.validate_reorder_level()
self.validate_warehouse_for_reorder()
self.validate_variants()
self.update_item_desc()
self.synced_with_hub = 0
self.validate_has_variants()
self.validate_stock_for_template_must_be_zero()
if not self.get("__islocal"):
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
@@ -77,7 +74,7 @@ class Item(WebsiteGenerator):
invalidate_cache_for_item(self)
self.validate_name_with_item_group()
self.update_item_price()
self.sync_variants()
self.update_variants()
def get_context(self, context):
context["parent_groups"] = get_parent_item_groups(self.item_group) + \
@@ -133,142 +130,6 @@ class Item(WebsiteGenerator):
if not matched:
frappe.throw(_("Default Unit of Measure can not be changed directly because you have already made some transaction(s) with another UOM. To change default UOM, use 'UOM Replace Utility' tool under Stock module."))
def validate_variants(self):
self.validate_variants_are_unique()
self.validate_stock_for_template_must_be_zero()
def validate_stock_for_template_must_be_zero(self):
if self.has_variants:
stock_in = frappe.db.sql_list("""select warehouse from tabBin
where item_code=%s and ifnull(actual_qty, 0) > 0""", self.name)
if stock_in:
frappe.throw(_("Item Template cannot have stock and varaiants. Please remove stock from warehouses {0}").format(", ".join(stock_in)),
ItemTemplateCannotHaveStock)
def validate_variants_are_unique(self):
if not self.has_variants:
self.variants = []
return
if self.variants:
if self.variant_of:
frappe.throw(_("Item cannot be a variant of a variant"))
variants, attributes = [], {}
for d in self.variants:
key = (d.item_attribute, d.item_attribute_value)
if key in variants:
frappe.throw(_("{0} {1} is entered more than once in Item Variants table")
.format(d.item_attribute, d.item_attribute_value), DuplicateVariant)
variants.append(key)
attributes.setdefault(d.item_attribute, [t.attribute_value for t in frappe.db.get_all("Item Attribute Value",
fields=["attribute_value"], filters={"parent": d.item_attribute })])
if d.item_attribute_value not in attributes.get(d.item_attribute):
frappe.throw(_("Attribute value {0} does not exist in Item Attribute Master.").format(d.item_attribute_value))
else:
frappe.throw(_("Please enter atleast one attribute row in Item Variants table"))
def sync_variants(self):
variant_item_codes = self.get_variant_item_codes()
# delete missing variants
existing_variants = [d.name for d in frappe.get_all("Item",
filters={"variant_of":self.name})]
updated, deleted = [], []
for existing_variant in existing_variants:
if existing_variant not in variant_item_codes:
frappe.delete_doc("Item", existing_variant)
deleted.append(existing_variant)
else:
self.update_variant(existing_variant)
updated.append(existing_variant)
inserted = []
for item_code in variant_item_codes:
if item_code not in existing_variants:
self.make_variant(item_code)
inserted.append(item_code)
if inserted:
frappe.msgprint(_("Item Variants {0} created").format(", ".join(inserted)))
if updated:
frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
if deleted:
frappe.msgprint(_("Item Variants {0} deleted").format(", ".join(deleted)))
def get_variant_item_codes(self):
"""Get all possible suffixes for variants"""
if not self.variants:
return []
self.variant_attributes = {}
variant_dict = {}
variant_item_codes = []
for d in self.variants:
variant_dict.setdefault(d.item_attribute, []).append(d.item_attribute_value)
all_attributes = [d.name for d in frappe.get_all("Item Attribute", order_by = "priority asc")]
# sort attributes by their priority
attributes = filter(None, map(lambda d: d if d in variant_dict else None, all_attributes))
def add_attribute_suffixes(item_code, my_attributes, attributes):
attr = frappe.get_doc("Item Attribute", attributes[0])
for value in attr.item_attribute_values:
if value.attribute_value in variant_dict[attr.name]:
_my_attributes = copy.deepcopy(my_attributes)
_my_attributes.append([attr.name, value.attribute_value])
if len(attributes) > 1:
add_attribute_suffixes(item_code + "-" + value.abbr, _my_attributes, attributes[1:])
else:
variant_item_codes.append(item_code + "-" + value.abbr)
self.variant_attributes[item_code + "-" + value.abbr] = _my_attributes
add_attribute_suffixes(self.name, [], attributes)
return variant_item_codes
def make_variant(self, item_code):
item = frappe.new_doc("Item")
item.item_code = item_code
self.copy_attributes_to_variant(self, item, insert=True)
item.insert()
def update_variant(self, item_code):
item = frappe.get_doc("Item", item_code)
item.item_code = item_code
self.copy_attributes_to_variant(self, item)
item.save()
def copy_attributes_to_variant(self, template, variant, insert=False):
from frappe.model import no_value_fields
for field in self.meta.fields:
if field.fieldtype not in no_value_fields and (insert or not field.no_copy)\
and field.fieldname not in ("item_code", "item_name"):
if variant.get(field.fieldname) != template.get(field.fieldname):
variant.set(field.fieldname, template.get(field.fieldname))
variant.__dirty = True
variant.description += "\n"
if not getattr(template, "variant_attributes", None):
template.get_variant_item_codes()
for attr in template.variant_attributes[variant.item_code]:
variant.description += "<p>" + attr[0] + ": " + attr[1] + "</p>"
variant.item_name = self.item_name + variant.item_code[len(self.name):]
variant.variant_of = template.name
variant.has_variants = 0
variant.show_in_website = 0
def update_template_tables(self):
template = frappe.get_doc("Item", self.variant_of)
@@ -313,15 +174,6 @@ class Item(WebsiteGenerator):
if bom_item not in (self.name, self.variant_of):
frappe.throw(_("Default BOM ({0}) must be active for this item or its template").format(bom_item))
if self.is_purchase_item != "Yes":
bom_mat = frappe.db.sql("""select distinct t1.parent
from `tabBOM Item` t1, `tabBOM` t2 where t2.name = t1.parent
and t1.item_code =%s and ifnull(t1.bom_no, '') = '' and t2.is_active = 1
and t2.docstatus = 1 and t1.docstatus =1 """, self.name)
if bom_mat and bom_mat[0][0]:
frappe.throw(_("Item must be a purchase item, as it is present in one or many Active BOMs"))
def fill_customer_code(self):
""" Append all the customer codes and insert into "customer_code" field of item table """
cust_code=[]
@@ -361,7 +213,8 @@ class Item(WebsiteGenerator):
vals.has_batch_no != self.has_batch_no or
cstr(vals.valuation_method) != cstr(self.valuation_method)):
if self.check_if_sle_exists() == "exists":
frappe.throw(_("As there are existing stock transactions for this item, you can not change the values of 'Has Serial No', 'Has Batch No', 'Is Stock Item' and 'Valuation Method'"))
frappe.throw(_("As there are existing stock transactions for this item, \
you can not change the values of 'Has Serial No', 'Has Batch No', 'Is Stock Item' and 'Valuation Method'"))
def validate_reorder_level(self):
if cint(self.apply_warehouse_wise_reorder_level):
@@ -460,9 +313,33 @@ class Item(WebsiteGenerator):
def update_item_desc(self):
if frappe.db.get_value('BOM',self.name, 'description') != self.description:
frappe.db.sql("""update `tabBOM` set description = %s where item = %s and docstatus < 2""",(self.description, self.name))
frappe.db.sql("""update `tabBOM Item` set description = %s where item_code = %s and docstatus < 2""",(self.description, self.name))
frappe.db.sql("""update `tabBOM Explosion Item` set description = %s where item_code = %s and docstatus < 2""",(self.description, self.name))
frappe.db.sql("""update `tabBOM Item` set description = %s where
item_code = %s and docstatus < 2""",(self.description, self.name))
frappe.db.sql("""update `tabBOM Explosion Item` set description = %s where
item_code = %s and docstatus < 2""",(self.description, self.name))
def update_variants(self):
if self.has_variants:
updated = []
variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name })
for d in variants:
update_variant(self.name, d)
updated.append(d.item_code)
frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
def validate_has_variants(self):
if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"):
if frappe.db.exists("Item", {"variant_of": self.name}):
frappe.throw(_("Item has variants."))
def validate_stock_for_template_must_be_zero(self):
if self.has_variants:
stock_in = frappe.db.sql_list("""select warehouse from tabBin
where item_code=%s and (ifnull(actual_qty, 0) > 0 or ifnull(ordered_qty, 0) > 0
or ifnull(reserved_qty, 0) > 0 or ifnull(indented_qty, 0) > 0 or ifnull(planned_qty, 0) > 0)""", self.name)
if stock_in:
frappe.throw(_("Item Template cannot have stock or Open Sales/Purchase/Production Orders."), ItemTemplateCannotHaveStock)
def validate_end_of_life(item_code, end_of_life=None, verbose=1):
if not end_of_life:
end_of_life = frappe.db.get_value("Item", item_code, "end_of_life")
@@ -567,3 +444,4 @@ def invalidate_cache_for_item(doc):
if doc.get("old_item_group") and doc.get("old_item_group") != doc.item_group:
invalidate_cache_for(doc, doc.old_item_group)

View File

@@ -6,7 +6,7 @@ import unittest
import frappe
from frappe.test_runner import make_test_records
from erpnext.stock.doctype.item.item import WarehouseNotSet, DuplicateVariant, ItemTemplateCannotHaveStock
from erpnext.stock.doctype.item.item import WarehouseNotSet, ItemTemplateCannotHaveStock
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
test_ignore = ["BOM"]
@@ -20,60 +20,14 @@ class TestItem(unittest.TestCase):
item.insert()
else:
item = frappe.get_doc("Item", item_code)
return item
def test_duplicate_variant(self):
item = frappe.copy_doc(test_records[11])
item.append("variants", {"item_attribute": "Test Size", "item_attribute_value": "Small"})
self.assertRaises(DuplicateVariant, item.insert)
def test_template_cannot_have_stock(self):
item = self.get_item(10)
se = make_stock_entry(item_code=item.name, target="Stores - _TC", qty=1, incoming_rate=1)
item.has_variants = 1
item.append("variants", {"item_attribute": "Test Size", "item_attribute_value": "Small"})
self.assertRaises(ItemTemplateCannotHaveStock, item.save)
def test_variant_item_codes(self):
item = self.get_item(11)
variants = ['_Test Variant Item-S', '_Test Variant Item-M', '_Test Variant Item-L']
self.assertEqual(item.get_variant_item_codes(), variants)
for v in variants:
self.assertTrue(frappe.db.get_value("Item", {"variant_of": item.name, "name": v}))
item.append("variants", {"item_attribute": "Test Colour", "item_attribute_value": "Red"})
item.append("variants", {"item_attribute": "Test Colour", "item_attribute_value": "Blue"})
item.append("variants", {"item_attribute": "Test Colour", "item_attribute_value": "Green"})
self.assertEqual(item.get_variant_item_codes(), ['_Test Variant Item-S-R',
'_Test Variant Item-S-G', '_Test Variant Item-S-B',
'_Test Variant Item-M-R', '_Test Variant Item-M-G',
'_Test Variant Item-M-B', '_Test Variant Item-L-R',
'_Test Variant Item-L-G', '_Test Variant Item-L-B'])
self.assertEqual(item.variant_attributes['_Test Variant Item-L-R'], [['Test Size', 'Large'], ['Test Colour', 'Red']])
self.assertEqual(item.variant_attributes['_Test Variant Item-S-G'], [['Test Size', 'Small'], ['Test Colour', 'Green']])
# check stock entry cannot be made
def test_stock_entry_cannot_be_made_for_template(self):
item = self.get_item(11)
se = frappe.new_doc("Stock Entry")
se.purpose = "Material Receipt"
se.append("items", {
"item_code": item.name,
"t_warehouse": "Stores - _TC",
"qty": 1,
"incoming_rate": 1
})
se.insert()
self.assertRaises(ItemTemplateCannotHaveStock, se.submit)
item = self.get_item(10)
se = make_stock_entry(item_code=item.name, target="Stores - _TC", qty=1, incoming_rate=1)
item.has_variants = 1
self.assertRaises(ItemTemplateCannotHaveStock, item.save)
def test_default_warehouse(self):
item = frappe.copy_doc(test_records[0])
item.is_stock_item = "Yes"

View File

@@ -273,11 +273,6 @@
"item_name": "_Test Variant Item",
"stock_uom": "_Test UOM",
"has_variants": 1,
"variants": [
{"item_attribute": "Test Size", "item_attribute_value": "Small"},
{"item_attribute": "Test Size", "item_attribute_value": "Medium"},
{"item_attribute": "Test Size", "item_attribute_value": "Large"}
],
"apply_warehouse_wise_reorder_level": 1,
"reorder_levels": [
{

View File

@@ -0,0 +1,56 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Manage Variants", {
onload: function(frm) {
var df = frappe.meta.get_docfield("Variant Attribute", "attribute_value");
df.on_make = function(field) {
$(field.input_area).addClass("ui-front");
field.$input.autocomplete({
minLength: 0,
minChars: 0,
source: function(request, response) {
frappe.call({
method:"frappe.client.get_list",
args:{
doctype:"Item Attribute Value",
filters: [
["parent","=", field.doc.attribute],
["attribute_value", "like", request.term + "%"]
],
fields: ["attribute_value"]
},
callback: function(r) {
response($.map(r.message, function(d) { return d.attribute_value; }));
}
});
},
select: function(event, ui) {
field.$input.val(ui.item.value);
field.$input.trigger("change");
}
});
}
},
refresh: function(frm) {
frm.disable_save();
frm.page.set_primary_action(__("Create Variants"), function() {
frappe.call({
method: "create_variants",
doc:frm.doc
})
});
},
item_code:function(frm) {
return frappe.call({
method: "get_item_details",
doc:frm.doc,
callback: function(r) {
refresh_field('attributes');
refresh_field('variants');
}
})
}
});

View File

@@ -0,0 +1,103 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"creation": "2015-05-19 05:39:59.345901",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"fields": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item Code",
"options": "Item",
"permlevel": 0,
"precision": "",
"reqd": 1
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"label": "Item Variant Attributes",
"permlevel": 0,
"precision": ""
},
{
"allow_on_submit": 0,
"fieldname": "attributes",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Attributes",
"no_copy": 0,
"options": "Variant Attribute",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0
},
{
"fieldname": "generate_combinations",
"fieldtype": "Button",
"label": "Generate Combinations",
"options": "generate_combinations",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"label": "Item Variants",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "variants",
"fieldtype": "Table",
"label": "Variants",
"options": "Manage Variants Item",
"permlevel": 0,
"precision": ""
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"in_create": 1,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"modified": "2015-06-30 13:40:59.946655",
"modified_by": "Administrator",
"module": "Stock",
"name": "Manage Variants",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "Material Master Manager",
"share": 1,
"write": 1
}
],
"read_only": 1,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
import copy
import json
class DuplicateAttribute(frappe.ValidationError): pass
class ManageVariants(Document):
def get_item_details(self):
self.clear_tables()
if self.item_code:
self.get_attributes()
self.get_variants()
def generate_combinations(self):
self.validate_attributes()
self.validate_template_item()
self.validate_attribute_values()
self.validate_attributes_are_unique()
self.get_variant_item_codes()
def create_variants(self):
self.sync_variants()
def clear_tables(self):
self.set('attributes', [])
self.set('variants', [])
def get_attributes(self):
attributes = {}
self.set('attributes', [])
for d in frappe.db.sql("""select attribute, attribute_value from `tabVariant Attribute` as attribute,
`tabItem` as item where attribute.parent= item.name and item.variant_of = %s""", self.item_code, as_dict=1):
attributes.setdefault(d.attribute, []).append(d.attribute_value)
for d in attributes:
attribute_values = set(attributes[d])
for value in attribute_values:
self.append('attributes',{"attribute": d, "attribute_value": value})
def get_variants(self):
variants = [d.name for d in frappe.get_all("Item",
filters={"variant_of":self.item_code})]
data = frappe.db.sql("""select parent, attribute, attribute_value from `tabVariant Attribute`""", as_dict=1)
for d in variants:
variant_attributes, attributes = "", []
for attribute in data:
if attribute.parent == d:
variant_attributes += attribute.attribute_value + " | "
attributes.append([attribute.attribute, attribute.attribute_value])
self.append('variants',{"variant": d, "variant_attributes": variant_attributes[: -3], "attributes": json.dumps(attributes)})
def validate_attributes(self):
if not self.attributes:
frappe.throw(_("Enter atleast one Attribute & its Value in Attribute table."))
def validate_template_item(self):
if not frappe.db.get_value("Item", self.item_code, "has_variants"):
frappe.throw(_("Selected Item cannot have Variants."))
if frappe.db.get_value("Item", self.item_code, "variant_of"):
frappe.throw(_("Item cannot be a variant of a variant"))
def validate_attribute_values(self):
attributes = {}
for t in frappe.db.get_all("Item Attribute Value", fields=["parent", "attribute_value"]):
attributes.setdefault(t.parent, []).append(t.attribute_value)
for d in self.attributes:
if d.attribute_value not in attributes.get(d.attribute):
frappe.throw(_("Attribute value {0} does not exist in Item Attribute Master.").format(d.attribute_value))
def validate_attributes_are_unique(self):
attributes = []
for d in self.attributes:
key = (d.attribute, d.attribute_value)
if key in attributes:
frappe.throw(_("{0} {1} is entered more than once in Attributes table")
.format(d.attribute, d.attribute_value), DuplicateAttribute)
attributes.append(key)
def get_variant_item_codes(self):
"""Get all possible suffixes for variants"""
variant_dict = {}
self.set('variants', [])
for d in self.attributes:
variant_dict.setdefault(d.attribute, []).append(d.attribute_value)
all_attributes = [d.name for d in frappe.get_all("Item Attribute", order_by = "priority asc")]
# sort attributes by their priority
attributes = filter(None, map(lambda d: d if d in variant_dict else None, all_attributes))
def add_attribute_suffixes(item_code, my_attributes, attributes):
attr = frappe.get_doc("Item Attribute", attributes[0])
for value in attr.item_attribute_values:
if value.attribute_value in variant_dict[attr.name]:
_my_attributes = copy.deepcopy(my_attributes)
_my_attributes.append([attr.name, value.attribute_value])
if len(attributes) > 1:
add_attribute_suffixes(item_code + "-" + value.abbr, _my_attributes, attributes[1:])
else:
variant_attributes = ""
for d in _my_attributes:
variant_attributes += d[1] + " | "
self.append('variants', {"variant": item_code + "-" + value.abbr,
"attributes": json.dumps(_my_attributes), "variant_attributes": variant_attributes[: -3]})
add_attribute_suffixes(self.item_code, [], attributes)
def sync_variants(self):
variant_item_codes = []
item_variants_attributes = {}
inserted, updated, old_variant_name, new_variant_name, deleted = [], [], [], [], []
for v in self.variants:
variant_item_codes.append(v.variant)
existing_variants = [d.name for d in frappe.get_all("Item",
filters={"variant_of":self.item_code})]
for d in existing_variants:
attributes = []
for attribute in frappe.db.sql("""select attribute, attribute_value from `tabVariant Attribute` where parent = %s""", d):
attributes.append([attribute[0], attribute[1]])
item_variants_attributes.setdefault(d, []).append(attributes)
for existing_variant in existing_variants:
if existing_variant not in variant_item_codes:
att = item_variants_attributes[existing_variant][0]
for variant in self.variants:
if sorted(json.loads(variant.attributes) ,key=lambda x: x[0]) == \
sorted(att ,key=lambda x: x[0]):
rename_variant(existing_variant, variant.variant)
old_variant_name.append(existing_variant)
new_variant_name.append(variant.variant)
if existing_variant not in old_variant_name:
delete_variant(existing_variant)
deleted.append(existing_variant)
for item_code in variant_item_codes:
if item_code not in existing_variants:
if item_code not in new_variant_name:
make_variant(self.item_code, item_code, self.variants)
inserted.append(item_code)
else:
update_variant(self.item_code, item_code, self.variants)
updated.append(item_code)
if inserted:
frappe.msgprint(_("Item Variants {0} created").format(", ".join(inserted)))
if updated:
frappe.msgprint(_("Item Variants {0} updated").format(", ".join(updated)))
if old_variant_name:
frappe.msgprint(_("Item Variants {0} renamed").format(", ".join(old_variant_name)))
if deleted:
frappe.msgprint(_("Item Variants {0} deleted").format(", ".join(deleted)))
def make_variant(item, variant_code, variant_attribute):
variant = frappe.new_doc("Item")
variant.item_code = variant_code
copy_attributes_to_variant(item, variant, variant_attribute, insert=True)
variant.insert()
def update_variant(item, variant_code, variant_attribute=None):
variant = frappe.get_doc("Item", variant_code)
copy_attributes_to_variant(item, variant, variant_attribute, insert=True)
variant.save()
def rename_variant(old_variant_code, new_variant_code):
frappe.rename_doc("Item", old_variant_code, new_variant_code)
def delete_variant(variant_code):
frappe.delete_doc("Item", variant_code)
def copy_attributes_to_variant(item, variant, variant_attribute=None, insert=False):
template = frappe.get_doc("Item", item)
from frappe.model import no_value_fields
for field in template.meta.fields:
if field.fieldtype not in no_value_fields and (insert or not field.no_copy)\
and field.fieldname not in ("item_code", "item_name"):
if variant.get(field.fieldname) != template.get(field.fieldname):
variant.set(field.fieldname, template.get(field.fieldname))
variant.item_name = template.item_name + variant.item_code[len(template.name):]
variant.variant_of = template.name
variant.has_variants = 0
variant.show_in_website = 0
if variant_attribute:
for d in variant_attribute:
if d.variant == variant.item_code:
variant.attributes= []
for a in json.loads(d.attributes):
variant.append('attributes', {"attribute": a[0], "attribute_value": a[1]})
if variant.attributes:
variant.description += "\n"
for d in variant.attributes:
variant.description += "<p>" + d.attribute + ": " + d.attribute_value + "</p>"

View File

@@ -0,0 +1,49 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from erpnext.stock.doctype.manage_variants.manage_variants import DuplicateAttribute
class TestManageVariants(unittest.TestCase):
def test_variant_item_codes(self):
manage_variant = frappe.new_doc("Manage Variants")
manage_variant.update({
"item": "_Test Variant Item",
"attributes": [
{
"attribute": "Test Size",
"attribute_value": "Small"
},
{
"attribute": "Test Size",
"attribute_value": "Large"
}
]
})
manage_variant.generate_combinations()
self.assertEqual(manage_variant.variants[0].variant, "_Test Variant Item-S")
self.assertEqual(manage_variant.variants[1].variant, "_Test Variant Item-L")
self.assertEqual(manage_variant.variants[0].variant_attributes, "Small")
self.assertEqual(manage_variant.variants[1].variant_attributes, "Large")
manage_variant.create_variants()
def test_attributes_are_unique(self):
manage_variant = frappe.new_doc("Manage Variants")
manage_variant.update({
"item": "_Test Variant Item",
"attributes": [
{
"attribute": "Test Size",
"attribute_value": "Small"
},
{
"attribute": "Test Size",
"attribute_value": "Small"
}
]
})
self.assertRaises(DuplicateAttribute, manage_variant.generate_combinations)

View File

@@ -0,0 +1,76 @@
{
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "",
"creation": "2015-05-19 05:55:31.155672",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Other",
"fields": [
{
"allow_on_submit": 0,
"fieldname": "variant",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Variant",
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"fieldname": "variant_attributes",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Variant Attributes",
"permlevel": 0,
"precision": "",
"read_only": 1
},
{
"fieldname": "attributes",
"fieldtype": "Text",
"hidden": 1,
"label": "attributes",
"permlevel": 0,
"precision": "",
"read_only": 1
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "",
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"modified": "2015-06-30 03:19:07.548196",
"modified_by": "Administrator",
"module": "Stock",
"name": "Manage Variants Item",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class ManageVariantsItem(Document):
pass

View File

@@ -180,7 +180,7 @@ class SerialNo(StockController):
where fieldname='serial_no' and fieldtype='Text'"""):
for item in frappe.db.sql("""select name, serial_no from `tab%s`
where serial_no like '%%%s%%'""" % (dt[0], old)):
where serial_no like '%%%s%%'""" % (dt[0], frappe.db.escape(old))):
serial_nos = map(lambda i: i==old and new or i, item[1].split('\n'))
frappe.db.sql("""update `tab%s` set serial_no = %s

View File

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import frappe.defaults
from frappe.utils import cstr, cint, flt, comma_or, get_datetime
from frappe.utils import cstr, cint, flt, comma_or, get_datetime, getdate
from frappe import _
from erpnext.stock.utils import get_incoming_rate
@@ -66,6 +66,7 @@ class StockEntry(StockController):
self.validate_valuation_rate()
self.set_total_incoming_outgoing_value()
self.set_total_amount()
self.validate_batch()
def on_submit(self):
self.update_stock_ledger()
@@ -359,8 +360,11 @@ class StockEntry(StockController):
if self.purpose == "Subcontract" and self.purchase_order:
purchase_order = frappe.get_doc("Purchase Order", self.purchase_order)
for se_item in self.items:
total_allowed = [d.required_qty for d in purchase_order.supplied_items \
if d.rm_item_code == se_item.item_code][0]
total_allowed = sum([flt(d.required_qty) for d in purchase_order.supplied_items \
if d.rm_item_code == se_item.item_code])
if not total_allowed:
frappe.throw(_("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}")
.format(se_item.item_code, self.purchase_order))
total_supplied = frappe.db.sql("""select sum(qty)
from `tabStock Entry Detail`, `tabStock Entry`
where `tabStock Entry`.purchase_order = %s
@@ -721,6 +725,13 @@ class StockEntry(StockController):
mreq_item.warehouse != (item.s_warehouse if self.purpose== "Material Issue" else item.t_warehouse):
frappe.throw(_("Item or Warehouse for row {0} does not match Material Request").format(item.idx),
frappe.MappingMismatchError)
def validate_batch(self):
if self.purpose == "Material Transfer for Manufacture":
for item in self.get("items"):
if item.batch_no:
if getdate(self.posting_date) > getdate(frappe.db.get_value("Batch", item.batch_no, "expiry_date")):
frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code))
@frappe.whitelist()
def get_party_details(ref_dt, ref_dn):

View File

@@ -72,6 +72,18 @@ class TestStockEntry(unittest.TestCase):
self._test_auto_material_request("_Test Item")
def test_auto_material_request_for_variant(self):
manage_variant = frappe.new_doc("Manage Variants")
manage_variant.update({
"item": "_Test Variant Item",
"attributes": [
{
"attribute": "Test Size",
"attribute_value": "Small"
}
]
})
manage_variant.generate_combinations()
manage_variant.create_variants()
self._test_auto_material_request("_Test Variant Item-S")
def _test_auto_material_request(self, item_code):

View File

@@ -100,7 +100,7 @@
{
"fieldname": "description",
"fieldtype": "Text",
"in_list_view": 1,
"in_list_view": 0,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
@@ -344,7 +344,7 @@
],
"idx": 1,
"istable": 1,
"modified": "2015-02-25 04:31:21.801200",
"modified": "2015-07-02 05:32:56.511570",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@@ -0,0 +1,78 @@
{
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "",
"creation": "2015-05-19 05:12:30.344797",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Other",
"fields": [
{
"allow_on_submit": 0,
"fieldname": "attribute",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Attribute",
"no_copy": 0,
"options": "Item Attribute",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"allow_on_submit": 0,
"fieldname": "attribute_value",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Attribute Value",
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "",
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"modified": "2015-05-20 06:16:16.803578",
"modified_by": "Administrator",
"module": "Stock",
"name": "Variant Attribute",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class VariantAttribute(Document):
pass

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import add_days, getdate, cint
from frappe.utils import add_days, getdate, cint, cstr
from frappe import throw, _
from erpnext.utilities.transaction_base import TransactionBase, delete_events
@@ -73,7 +73,7 @@ class MaintenanceSchedule(TransactionBase):
"owner": email_map[d.sales_person] or self.owner,
"subject": description,
"description": description,
"starts_on": key["scheduled_date"] + " 10:00:00",
"starts_on": cstr(key["scheduled_date"]) + " 10:00:00",
"event_type": "Private",
"ref_type": self.doctype,
"ref_name": self.name

View File

@@ -9,5 +9,7 @@
{%= doc.get_formatted(df.fieldname) %}
</div>
</div>
{% } else if (df.fieldname==="description") { %}
<p>{{ doc.description }}</p>
{% } %}
{% }); %}

View File

@@ -1,4 +1,6 @@
{% var visible_columns = row.get_visible_columns(["item_code", "qty", "rate", "amount", "stock_uom", "uom", "discount_percentage", "schedule_date", "warehouse", "against_sales_order", "sales_order"]); %}
{% var visible_columns = row.get_visible_columns(["item_code", "qty", "rate", "amount",
"stock_uom", "uom", "discount_percentage", "schedule_date", "warehouse",
"against_sales_order", "sales_order"]); %}
{% if(!doc) { %}
<div class="row">
@@ -48,12 +50,7 @@
{% if(doc.item_name != doc.item_code && in_list(visible_column_fieldnames, "item_name")) { %}
<br>{%= doc.item_name %}{% } %}
{% if((doc.description != doc.item_code != doc.item_name) &&
in_list(visible_column_fieldnames, "description")) { %}
<br>
<strong>Description: </strong>
{%= doc.get_formatted("description") %}{% } %}
{% include "templates/form_grid/includes/visible_cols.html" %}
</div>

View File

@@ -1,6 +1,6 @@
<div class="web-list-item">
<div class="row">
<div class="col-sm-8">
<div class="col-sm-6">
<a class="no-decoration" href="/issues?name={{ doc.name }}" no-pjax>
{{ doc.subject }}
</a>
@@ -9,6 +9,11 @@
<span class="indicator {{ "red" if doc.status=="Open" else "blue" }}">
{{ doc.status }}</span>
</div>
<div class="col-sm-2">
<a class="text-muted text-right" href="/issues?name={{ doc.name }}" no-pjax>
{{ doc.name }}
</a>
</div>
<div class="col-sm-2 text-muted text-right small">
{{ frappe.format_date(doc.creation) }}
</div>

View File

@@ -22,16 +22,21 @@ class TransactionBase(StatusUpdater):
self.posting_time = now_datetime().strftime('%H:%M:%S')
def add_calendar_event(self, opts, force=False):
if self.contact_by != cstr(self._prev.contact_by) or \
self.contact_date != cstr(self._prev.contact_date) or force:
if cstr(self.contact_by) != cstr(self._prev.contact_by) or \
cstr(self.contact_date) != cstr(self._prev.contact_date) or force:
self.delete_events()
self._add_calendar_event(opts)
def delete_events(self):
frappe.delete_doc("Event", frappe.db.sql_list("""select name from `tabEvent`
where ref_type=%s and ref_name=%s""", (self.doctype, self.name)),
ignore_permissions=True)
events = frappe.db.sql_list("""select name from `tabEvent`
where ref_type=%s and ref_name=%s""", (self.doctype, self.name))
if events:
frappe.db.sql("delete from `tabEvent` where name in (%s)"
.format(", ".join(['%s']*len(events))), tuple(events))
frappe.db.sql("delete from `tabEvent Role` where parent in (%s)"
.format(", ".join(['%s']*len(events))), tuple(events))
def _add_calendar_event(self, opts):
opts = frappe._dict(opts)

View File

@@ -1,6 +1,6 @@
from setuptools import setup, find_packages
version = "5.0.27"
version = "5.1.2"
with open("requirements.txt", "r") as f:
install_requires = f.readlines()